From 0e4760c18e66d68573081de3c7bc2e99f9b07c4b Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 20 May 2026 09:46:28 -0600 Subject: [PATCH 01/31] local build --- package-lock.json | 56 +++++++++++------------------------------------ package.json | 2 +- 2 files changed, 14 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5466b10e..0aeb1a3db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@fastify/type-provider-typebox": "5.2.0", "@sinclair/typebox": "0.34.48", "@stacks/api-toolkit": "1.13.0", - "@stacks/codec": "1.8.0-pox5.1", + "@stacks/codec": "file:../stacks-codec-js", "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", @@ -75,20 +75,23 @@ }, "../stacks-codec-js": { "name": "@stacks/codec", - "version": "1.6.0", - "extraneous": true, + "version": "1.8.0-pox5.1", "license": "GPL-3.0", "dependencies": { - "@types/node": "^16.11.26", + "@types/node": "^24.0.0", "detect-libc": "^2.0.1" }, "devDependencies": { "@stacks/prettier-config": "^0.0.10", - "@types/jest": "^27.4.1", + "@types/jest": "^29.5.14", "cargo-cp-artifact": "^0.1.9", - "jest": "^27.5.1", - "ts-jest": "^27.1.4", - "typescript": "^4.6.3" + "esbuild": "^0.25.0", + "jest": "^29.7.0", + "ts-jest": "^29.3.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20" } }, "node_modules/@babel/code-frame": { @@ -1717,26 +1720,8 @@ "license": "MIT" }, "node_modules/@stacks/codec": { - "version": "1.8.0-pox5.1", - "resolved": "https://registry.npmjs.org/@stacks/codec/-/codec-1.8.0-pox5.1.tgz", - "integrity": "sha512-HjGss+GqxYi0bjcd5PVo9RAutWm38rA1yORrszNxjA8QGeIp1P3eMrYYF+tPnM8CDrNpOAe+i9Mqu5goGlTMgw==", - "license": "GPL-3.0", - "dependencies": { - "@types/node": "^20.14.0", - "detect-libc": "^2.0.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@stacks/codec/node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } + "resolved": "../stacks-codec-js", + "link": true }, "node_modules/@stacks/common": { "version": "7.3.1", @@ -3416,15 +3401,6 @@ "node": ">=6" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -7232,12 +7208,6 @@ "node": ">=18.17" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 76a2c6e66..f52009fec 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@fastify/type-provider-typebox": "5.2.0", "@sinclair/typebox": "0.34.48", "@stacks/api-toolkit": "1.13.0", - "@stacks/codec": "1.8.0-pox5.1", + "@stacks/codec": "file:../stacks-codec-js", "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", From 554a960fadfe0b77bfeae7d006bc361526fd9181 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 20 May 2026 13:28:15 -0600 Subject: [PATCH 02/31] use codec for pox4 events --- src/api/controllers/db-controller.ts | 32 +- src/api/routes/v1/pox.ts | 8 +- src/datastore/common.ts | 179 +----- src/datastore/helpers.ts | 192 +++---- src/datastore/pg-store-v2.ts | 12 +- src/datastore/pg-store.ts | 118 ++-- src/datastore/pg-write-store.ts | 64 +-- src/event-stream/event-server.ts | 75 +-- .../pox-constants.ts} | 21 +- src/event-stream/pox-event-parsing.ts | 512 ------------------ src/event-stream/reader.ts | 29 +- .../pox-4-burnchain-delegate-stx.test.ts | 2 +- 12 files changed, 287 insertions(+), 957 deletions(-) rename src/{pox-helpers.ts => event-stream/pox-constants.ts} (58%) delete mode 100644 src/event-stream/pox-event-parsing.ts diff --git a/src/api/controllers/db-controller.ts b/src/api/controllers/db-controller.ts index 5c8e57d90..675839e5f 100644 --- a/src/api/controllers/db-controller.ts +++ b/src/api/controllers/db-controller.ts @@ -9,6 +9,7 @@ import { decodeClarityValueToRepr, decodeClarityValueToTypeName, decodePostConditions, + Pox4EventName, } from '@stacks/codec'; import { BlockIdentifier, @@ -23,7 +24,7 @@ import { DbTxTypeId, DbSearchResultWithMetadata, BaseTx, - DbPoxSyntheticEvent, + DbPox4SyntheticEvent, } from '../../datastore/common.js'; import { unwrapOptional, FoundOrNot, unixEpochToIso, EMPTY_HASH_256 } from '../../helpers.js'; import { @@ -31,7 +32,6 @@ import { serializePostConditionMode, } from '../serializers/v1/post-conditions.js'; import { PgStore } from '../../datastore/pg-store.js'; -import { SyntheticPoxEventName } from '../../pox-helpers.js'; import { logger } from '@stacks/api-toolkit'; import { AbstractMempoolTransaction, @@ -225,7 +225,7 @@ export function getAssetEventTypeString( } } -export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { +export function parsePox4SyntheticEvent(poxEvent: DbPox4SyntheticEvent) { const baseInfo = { block_height: poxEvent.block_height, tx_id: poxEvent.tx_id, @@ -240,7 +240,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { name: poxEvent.name, }; switch (poxEvent.name) { - case SyntheticPoxEventName.HandleUnlock: { + case Pox4EventName.HandleUnlock: { return { ...baseInfo, data: { @@ -249,7 +249,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.StackStx: { + case Pox4EventName.StackStx: { return { ...baseInfo, data: { @@ -263,7 +263,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.StackIncrease: { + case Pox4EventName.StackIncrease: { return { ...baseInfo, data: { @@ -275,7 +275,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.StackExtend: { + case Pox4EventName.StackExtend: { return { ...baseInfo, data: { @@ -287,7 +287,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.DelegateStx: { + case Pox4EventName.DelegateStx: { return { ...baseInfo, data: { @@ -299,7 +299,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.DelegateStackStx: { + case Pox4EventName.DelegateStackStx: { return { ...baseInfo, data: { @@ -313,7 +313,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.DelegateStackIncrease: { + case Pox4EventName.DelegateStackIncrease: { return { ...baseInfo, data: { @@ -325,7 +325,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.DelegateStackExtend: { + case Pox4EventName.DelegateStackExtend: { return { ...baseInfo, data: { @@ -337,7 +337,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.StackAggregationCommit: { + case Pox4EventName.StackAggregationCommit: { return { ...baseInfo, data: { @@ -349,7 +349,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.StackAggregationCommitIndexed: { + case Pox4EventName.StackAggregationCommitIndexed: { return { ...baseInfo, data: { @@ -361,7 +361,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.StackAggregationIncrease: { + case Pox4EventName.StackAggregationIncrease: { return { ...baseInfo, data: { @@ -372,7 +372,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }, }; } - case SyntheticPoxEventName.RevokeDelegateStx: { + case Pox4EventName.RevokeDelegateStx: { return { ...baseInfo, data: { @@ -383,7 +383,7 @@ export function parsePoxSyntheticEvent(poxEvent: DbPoxSyntheticEvent) { }; } default: - throw new Error(`Unexpected Pox2 event name ${(poxEvent as DbPoxSyntheticEvent).name}`); + throw new Error(`Unexpected Pox4 event name ${(poxEvent as DbPox4SyntheticEvent).name}`); } } diff --git a/src/api/routes/v1/pox.ts b/src/api/routes/v1/pox.ts index 738d374f9..7d52f4395 100644 --- a/src/api/routes/v1/pox.ts +++ b/src/api/routes/v1/pox.ts @@ -1,5 +1,5 @@ import { getPagingQueryLimit, parsePagingQueryInput, ResourceType } from '../../pagination.js'; -import { parsePoxSyntheticEvent } from '../../controllers/db-controller.js'; +import { parsePox4SyntheticEvent } from '../../controllers/db-controller.js'; import { getBlockParams, validatePrincipal, validateRequestHexInput } from '../../query-helpers.js'; import { handleChainTipCache } from '../../controllers/cache-controller.js'; import { FastifyPluginAsync } from 'fastify'; @@ -69,7 +69,7 @@ export const PoxRoutes: FastifyPluginAsync< limit, poxTable, }); - const parsedResult = queryResults.map(r => parsePoxSyntheticEvent(r)); + const parsedResult = queryResults.map(r => parsePox4SyntheticEvent(r)); const response = { limit, offset, @@ -105,7 +105,7 @@ export const PoxRoutes: FastifyPluginAsync< if (!queryResults.found) { throw new NotFoundError(`could not find transaction by ID`); } - const parsedResult = queryResults.result.map(r => parsePoxSyntheticEvent(r)); + const parsedResult = queryResults.result.map(r => parsePox4SyntheticEvent(r)); const response = { results: parsedResult, }; @@ -139,7 +139,7 @@ export const PoxRoutes: FastifyPluginAsync< if (!queryResults.found) { throw new NotFoundError(`could not find principal`); } - const parsedResult = queryResults.result.map(r => parsePoxSyntheticEvent(r)); + const parsedResult = queryResults.result.map(r => parsePox4SyntheticEvent(r)); const response = { results: parsedResult, }; diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 18aa7d369..08b917076 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1,5 +1,5 @@ +import { Pox4Event } from '@stacks/codec'; import { Block } from '../api/schemas/v1/entities/block.js'; -import { SyntheticPoxEventName } from '../pox-helpers.js'; import { PgBytea, PgJsonb, PgNumeric } from '@stacks/api-toolkit'; export interface DbBlock { @@ -388,163 +388,9 @@ export interface DbEventBase { canonical: boolean; } -export type PoxSyntheticEventTable = 'pox2_events' | 'pox3_events' | 'pox4_events'; +export type Pox4SyntheticEventTable = 'pox2_events' | 'pox3_events' | 'pox4_events'; -export interface DbPoxSyntheticBaseEventData { - stacker: string; - locked: bigint; - balance: bigint; - burnchain_unlock_height: bigint; - pox_addr: string | null; - pox_addr_raw: string | null; -} - -export interface DbPoxSyntheticHandleUnlockEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.HandleUnlock; - data: { - first_cycle_locked: bigint; - first_unlocked_cycle: bigint; - }; -} - -export interface DbPoxSyntheticStackStxEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.StackStx; - data: { - lock_amount: bigint; - lock_period: bigint; - start_burn_height: bigint; - unlock_burn_height: bigint; - signer_key: string | null; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticStackIncreaseEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.StackIncrease; - data: { - increase_by: bigint; - total_locked: bigint; - signer_key: string | null; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticStackExtendEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.StackExtend; - data: { - extend_count: bigint; - unlock_burn_height: bigint; - signer_key: string | null; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticDelegateStxEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.DelegateStx; - data: { - amount_ustx: bigint; - delegate_to: string; - unlock_burn_height: bigint | null; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticDelegateStackStxEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.DelegateStackStx; - data: { - lock_amount: bigint; - unlock_burn_height: bigint; - start_burn_height: bigint; - lock_period: bigint; - delegator: string; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticDelegateStackIncreaseEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.DelegateStackIncrease; - data: { - increase_by: bigint; - total_locked: bigint; - delegator: string; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticDelegateStackExtendEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.DelegateStackExtend; - data: { - unlock_burn_height: bigint; - extend_count: bigint; - delegator: string; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticStackAggregationCommitEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.StackAggregationCommit; - data: { - reward_cycle: bigint; - amount_ustx: bigint; - signer_key: string | null; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticStackAggregationCommitIndexedEvent - extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.StackAggregationCommitIndexed; - data: { - reward_cycle: bigint; - amount_ustx: bigint; - signer_key: string | null; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticStackAggregationIncreaseEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.StackAggregationIncrease; - data: { - reward_cycle: bigint; - amount_ustx: bigint; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export interface DbPoxSyntheticRevokeDelegateStxEvent extends DbPoxSyntheticBaseEventData { - name: SyntheticPoxEventName.RevokeDelegateStx; - data: { - delegate_to: string; - end_cycle_id: bigint | null; - start_cycle_id: bigint | null; - }; -} - -export type DbPoxSyntheticEventData = - | DbPoxSyntheticHandleUnlockEvent - | DbPoxSyntheticStackStxEvent - | DbPoxSyntheticStackIncreaseEvent - | DbPoxSyntheticStackExtendEvent - | DbPoxSyntheticDelegateStxEvent - | DbPoxSyntheticDelegateStackStxEvent - | DbPoxSyntheticDelegateStackIncreaseEvent - | DbPoxSyntheticDelegateStackExtendEvent - | DbPoxSyntheticStackAggregationCommitEvent - | DbPoxSyntheticStackAggregationCommitIndexedEvent - | DbPoxSyntheticStackAggregationIncreaseEvent - | DbPoxSyntheticRevokeDelegateStxEvent; - -export type DbPoxSyntheticEvent = DbEventBase & DbPoxSyntheticEventData; +export type DbPox4SyntheticEvent = DbEventBase & Pox4Event; export interface DbPoxStacker { stacker: string; @@ -678,9 +524,10 @@ export interface DataStoreTxEventData { smartContracts: DbSmartContract[]; names: DbBnsName[]; namespaces: DbBnsNamespace[]; - pox2Events: DbPoxSyntheticEvent[]; - pox3Events: DbPoxSyntheticEvent[]; - pox4Events: DbPoxSyntheticEvent[]; + pox2Events: DbPox4SyntheticEvent[]; + pox3Events: DbPox4SyntheticEvent[]; + pox4Events: DbPox4SyntheticEvent[]; + pox5Events: DbPox4SyntheticEvent[]; } export interface DataStoreAttachmentData { @@ -1455,7 +1302,7 @@ export interface PoxSyntheticEventQueryResult { start_cycle_id?: string | null; } -export interface PoxSyntheticEventInsertValues { +export interface Pox4SyntheticEventInsertValues { event_index: number; tx_id: PgBytea; tx_index: number; @@ -1520,6 +1367,16 @@ export interface PoxSyntheticEventInsertValues { start_cycle_id?: PgNumeric | null; } +export interface Pox5SyntheticEventInsertValues { + event_index: number; + tx_id: PgBytea; + tx_index: number; + block_height: number; + index_block_hash: PgBytea; + parent_index_block_hash: PgBytea; + microblock_hash: PgBytea; +} + export interface NftEventInsertValues { event_index: number; tx_id: PgBytea; diff --git a/src/datastore/helpers.ts b/src/datastore/helpers.ts index 6a0feb320..ae1da21a5 100644 --- a/src/datastore/helpers.ts +++ b/src/datastore/helpers.ts @@ -15,19 +15,7 @@ import { DbMempoolTxRaw, DbMicroblock, DbNftEvent, - DbPoxSyntheticBaseEventData, - DbPoxSyntheticDelegateStackExtendEvent, - DbPoxSyntheticDelegateStackIncreaseEvent, - DbPoxSyntheticDelegateStackStxEvent, - DbPoxSyntheticDelegateStxEvent, - DbPoxSyntheticEvent, - DbPoxSyntheticHandleUnlockEvent, - DbPoxSyntheticStackAggregationCommitEvent, - DbPoxSyntheticStackAggregationCommitIndexedEvent, - DbPoxSyntheticStackAggregationIncreaseEvent, - DbPoxSyntheticStackExtendEvent, - DbPoxSyntheticStackIncreaseEvent, - DbPoxSyntheticStackStxEvent, + DbPox4SyntheticEvent, DbSmartContract, DbSmartContractEvent, DbStxEvent, @@ -42,7 +30,6 @@ import { MicroblockQueryResult, PoxSyntheticEventQueryResult, TxQueryResult, - DbPoxSyntheticRevokeDelegateStxEvent, ReOrgUpdatedEntities, AddressTransfersTxQueryResult, DbTxWithAddressTransfers, @@ -51,16 +38,31 @@ import { CoreNodeParsedTxMessage } from '../event-stream/core-node-message.js'; import { decodeClarityValueToRepr, PostConditionAuthFlag, + Pox4EventName, PrincipalTypeID, TxPayloadTypeID, } from '@stacks/codec'; -import type { DecodedTxResult } from '@stacks/codec'; +import type { + DecodedTxResult, + Pox4EventBase, + Pox4EventDelegateStackExtend, + Pox4EventDelegateStackIncrease, + Pox4EventDelegateStackStx, + Pox4EventDelegateStx, + Pox4EventHandleUnlock, + Pox4EventRevokeDelegateStx, + Pox4EventStackAggregationCommit, + Pox4EventStackAggregationCommitIndexed, + Pox4EventStackAggregationIncrease, + Pox4EventStackExtend, + Pox4EventStackIncrease, + Pox4EventStackStx, +} from '@stacks/codec'; import { getTxSenderAddress } from '../event-stream/reader.js'; import postgres from 'postgres'; import * as prom from 'prom-client'; import { getAssetEventTypeString } from '../api/controllers/db-controller.js'; import { PgStoreEventEmitter } from './pg-store-event-emitter.js'; -import { SyntheticPoxEventName } from '../pox-helpers.js'; import { logger, PgSqlClient } from '@stacks/api-toolkit'; import PQueue from 'p-queue'; import { DropMempoolTxReasonType, NewBlockTransactionStatus } from '@stacks/node-publisher-client'; @@ -646,7 +648,7 @@ export function parseDbEvents( return events; } -export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbPoxSyntheticEvent { +export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbPox4SyntheticEvent { const baseEvent: DbEventBase = { event_index: row.event_index, tx_id: row.tx_id, @@ -654,23 +656,24 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP block_height: row.block_height, canonical: row.canonical, }; - const basePoxEvent: DbPoxSyntheticBaseEventData = { + const basePoxEvent: Pox4EventBase = { + pox_version: 'pox4', stacker: row.stacker, - locked: BigInt(row.locked ?? 0), - balance: BigInt(row.balance), - burnchain_unlock_height: BigInt(row.burnchain_unlock_height), + locked: row.locked ?? 0, + balance: row.balance, + burnchain_unlock_height: row.burnchain_unlock_height, pox_addr: row.pox_addr ?? null, pox_addr_raw: row.pox_addr_raw ?? null, }; - const rowName = row.name as SyntheticPoxEventName; + const rowName = row.name as Pox4EventName; switch (rowName) { - case SyntheticPoxEventName.HandleUnlock: { - const eventData: DbPoxSyntheticHandleUnlockEvent = { + case Pox4EventName.HandleUnlock: { + const eventData: Pox4EventHandleUnlock = { ...basePoxEvent, name: rowName, data: { - first_cycle_locked: BigInt(unwrapOptionalProp(row, 'first_unlocked_cycle')), - first_unlocked_cycle: BigInt(unwrapOptionalProp(row, 'first_unlocked_cycle')), + first_cycle_locked: unwrapOptionalProp(row, 'first_unlocked_cycle'), + first_unlocked_cycle: unwrapOptionalProp(row, 'first_unlocked_cycle'), }, }; return { @@ -678,18 +681,18 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.StackStx: { - const eventData: DbPoxSyntheticStackStxEvent = { + case Pox4EventName.StackStx: { + const eventData: Pox4EventStackStx = { ...basePoxEvent, name: rowName, data: { - lock_amount: BigInt(unwrapOptionalProp(row, 'lock_amount')), - lock_period: BigInt(unwrapOptionalProp(row, 'lock_period')), - start_burn_height: BigInt(unwrapOptionalProp(row, 'start_burn_height')), - unlock_burn_height: BigInt(unwrapOptionalProp(row, 'unlock_burn_height')), + lock_amount: unwrapOptionalProp(row, 'lock_amount'), + lock_period: unwrapOptionalProp(row, 'lock_period'), + start_burn_height: unwrapOptionalProp(row, 'start_burn_height'), + unlock_burn_height: unwrapOptionalProp(row, 'unlock_burn_height'), signer_key: row.signer_key ?? null, - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -697,16 +700,16 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.StackIncrease: { - const eventData: DbPoxSyntheticStackIncreaseEvent = { + case Pox4EventName.StackIncrease: { + const eventData: Pox4EventStackIncrease = { ...basePoxEvent, name: rowName, data: { - increase_by: BigInt(unwrapOptionalProp(row, 'increase_by')), - total_locked: BigInt(unwrapOptionalProp(row, 'total_locked')), + increase_by: unwrapOptionalProp(row, 'increase_by'), + total_locked: unwrapOptionalProp(row, 'total_locked'), signer_key: row.signer_key ?? null, - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -714,16 +717,16 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.StackExtend: { - const eventData: DbPoxSyntheticStackExtendEvent = { + case Pox4EventName.StackExtend: { + const eventData: Pox4EventStackExtend = { ...basePoxEvent, name: rowName, data: { - extend_count: BigInt(unwrapOptionalProp(row, 'extend_count')), - unlock_burn_height: BigInt(unwrapOptionalProp(row, 'unlock_burn_height')), + extend_count: unwrapOptionalProp(row, 'extend_count'), + unlock_burn_height: unwrapOptionalProp(row, 'unlock_burn_height'), signer_key: row.signer_key ?? null, - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -731,18 +734,18 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.DelegateStx: { - const eventData: DbPoxSyntheticDelegateStxEvent = { + case Pox4EventName.DelegateStx: { + const eventData: Pox4EventDelegateStx = { ...basePoxEvent, name: rowName, data: { - amount_ustx: BigInt(unwrapOptionalProp(row, 'amount_ustx')), + amount_ustx: unwrapOptionalProp(row, 'amount_ustx'), delegate_to: unwrapOptionalProp(row, 'delegate_to'), unlock_burn_height: row.unlock_burn_height - ? BigInt(unwrapOptionalProp(row, 'unlock_burn_height')) + ? unwrapOptionalProp(row, 'unlock_burn_height') : null, - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -750,18 +753,18 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.DelegateStackStx: { - const eventData: DbPoxSyntheticDelegateStackStxEvent = { + case Pox4EventName.DelegateStackStx: { + const eventData: Pox4EventDelegateStackStx = { ...basePoxEvent, name: rowName, data: { - lock_amount: BigInt(unwrapOptionalProp(row, 'lock_amount')), - unlock_burn_height: BigInt(unwrapOptionalProp(row, 'unlock_burn_height')), - start_burn_height: BigInt(unwrapOptionalProp(row, 'start_burn_height')), - lock_period: BigInt(unwrapOptionalProp(row, 'lock_period')), + lock_amount: unwrapOptionalProp(row, 'lock_amount'), + unlock_burn_height: unwrapOptionalProp(row, 'unlock_burn_height'), + start_burn_height: unwrapOptionalProp(row, 'start_burn_height'), + lock_period: unwrapOptionalProp(row, 'lock_period'), delegator: unwrapOptionalProp(row, 'delegator'), - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -769,16 +772,16 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.DelegateStackIncrease: { - const eventData: DbPoxSyntheticDelegateStackIncreaseEvent = { + case Pox4EventName.DelegateStackIncrease: { + const eventData: Pox4EventDelegateStackIncrease = { ...basePoxEvent, name: rowName, data: { - increase_by: BigInt(unwrapOptionalProp(row, 'increase_by')), - total_locked: BigInt(unwrapOptionalProp(row, 'total_locked')), + increase_by: unwrapOptionalProp(row, 'increase_by'), + total_locked: unwrapOptionalProp(row, 'total_locked'), delegator: unwrapOptionalProp(row, 'delegator'), - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -786,16 +789,16 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.DelegateStackExtend: { - const eventData: DbPoxSyntheticDelegateStackExtendEvent = { + case Pox4EventName.DelegateStackExtend: { + const eventData: Pox4EventDelegateStackExtend = { ...basePoxEvent, name: rowName, data: { - unlock_burn_height: BigInt(unwrapOptionalProp(row, 'unlock_burn_height')), - extend_count: BigInt(unwrapOptionalProp(row, 'extend_count')), + unlock_burn_height: unwrapOptionalProp(row, 'unlock_burn_height'), + extend_count: unwrapOptionalProp(row, 'extend_count'), delegator: unwrapOptionalProp(row, 'delegator'), - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -803,16 +806,16 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.StackAggregationCommit: { - const eventData: DbPoxSyntheticStackAggregationCommitEvent = { + case Pox4EventName.StackAggregationCommit: { + const eventData: Pox4EventStackAggregationCommit = { ...basePoxEvent, name: rowName, data: { - reward_cycle: BigInt(unwrapOptionalProp(row, 'reward_cycle')), - amount_ustx: BigInt(unwrapOptionalProp(row, 'amount_ustx')), + reward_cycle: unwrapOptionalProp(row, 'reward_cycle'), + amount_ustx: unwrapOptionalProp(row, 'amount_ustx'), signer_key: row.signer_key ?? null, - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -820,16 +823,16 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.StackAggregationCommitIndexed: { - const eventData: DbPoxSyntheticStackAggregationCommitIndexedEvent = { + case Pox4EventName.StackAggregationCommitIndexed: { + const eventData: Pox4EventStackAggregationCommitIndexed = { ...basePoxEvent, name: rowName, data: { - reward_cycle: BigInt(unwrapOptionalProp(row, 'reward_cycle')), - amount_ustx: BigInt(unwrapOptionalProp(row, 'amount_ustx')), + reward_cycle: unwrapOptionalProp(row, 'reward_cycle'), + amount_ustx: unwrapOptionalProp(row, 'amount_ustx'), signer_key: row.signer_key ?? null, - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -837,15 +840,15 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.StackAggregationIncrease: { - const eventData: DbPoxSyntheticStackAggregationIncreaseEvent = { + case Pox4EventName.StackAggregationIncrease: { + const eventData: Pox4EventStackAggregationIncrease = { ...basePoxEvent, name: rowName, data: { - reward_cycle: BigInt(unwrapOptionalProp(row, 'reward_cycle')), - amount_ustx: BigInt(unwrapOptionalProp(row, 'amount_ustx')), - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + reward_cycle: unwrapOptionalProp(row, 'reward_cycle'), + amount_ustx: unwrapOptionalProp(row, 'amount_ustx'), + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -853,14 +856,14 @@ export function parseDbPoxSyntheticEvent(row: PoxSyntheticEventQueryResult): DbP ...eventData, }; } - case SyntheticPoxEventName.RevokeDelegateStx: { - const eventData: DbPoxSyntheticRevokeDelegateStxEvent = { + case Pox4EventName.RevokeDelegateStx: { + const eventData: Pox4EventRevokeDelegateStx = { ...basePoxEvent, name: rowName, data: { delegate_to: unwrapOptionalProp(row, 'delegate_to'), - end_cycle_id: row.end_cycle_id ? BigInt(row.end_cycle_id) : null, - start_cycle_id: row.start_cycle_id ? BigInt(row.start_cycle_id) : null, + end_cycle_id: row.end_cycle_id ? row.end_cycle_id : null, + start_cycle_id: row.start_cycle_id ? row.start_cycle_id : null, }, }; return { @@ -1305,6 +1308,7 @@ export function markBlockUpdateDataAsNonCanonical(data: DataStoreBlockUpdateData pox2Events: tx.pox2Events.map(e => ({ ...e, canonical: false })), pox3Events: tx.pox3Events.map(e => ({ ...e, canonical: false })), pox4Events: tx.pox4Events.map(e => ({ ...e, canonical: false })), + pox5Events: tx.pox5Events.map(e => ({ ...e, canonical: false })), })); data.minerRewards = data.minerRewards.map(mr => ({ ...mr, canonical: false })); } diff --git a/src/datastore/pg-store-v2.ts b/src/datastore/pg-store-v2.ts index a129c0dc4..35e7e4d52 100644 --- a/src/datastore/pg-store-v2.ts +++ b/src/datastore/pg-store-v2.ts @@ -48,7 +48,7 @@ import { parseDbPoxSyntheticEvent, prefixedCols, } from './helpers.js'; -import { SyntheticPoxEventName } from '../pox-helpers.js'; +import { Pox4EventName } from '@stacks/codec'; async function assertTxIdExists(sql: PgSqlClient, tx_id: string) { const txCheck = await sql`SELECT tx_id FROM txs WHERE tx_id = ${tx_id} LIMIT 1`; @@ -1194,21 +1194,19 @@ export class PgStoreV2 extends BasePgStoreModule { AND block_height <= ${blockHeight} AND ( (name != ${ - SyntheticPoxEventName.HandleUnlock + Pox4EventName.HandleUnlock } AND burnchain_unlock_height >= ${burnBlockHeight}) OR - (name = ${ - SyntheticPoxEventName.HandleUnlock - } AND burnchain_unlock_height < ${burnBlockHeight}) + (name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${burnBlockHeight}) ) ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC LIMIT 1 `; if (pox4EventQuery.length > 0) { const pox4Event = parseDbPoxSyntheticEvent(pox4EventQuery[0]); - if (pox4Event.name !== SyntheticPoxEventName.HandleUnlock) { + if (pox4Event.name !== Pox4EventName.HandleUnlock) { lockTxId = pox4Event.tx_id; - locked = pox4Event.locked; + locked = BigInt(pox4Event.locked); burnchainUnlockHeight = Number(pox4Event.burnchain_unlock_height); lockHeight = pox4Event.block_height; diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 791719ae5..63b7ade9d 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -61,9 +61,9 @@ import { RawTxQueryResult, StxUnlockEvent, TransferQueryResult, - PoxSyntheticEventTable, + Pox4SyntheticEventTable, DbPoxStacker, - DbPoxSyntheticEvent, + DbPox4SyntheticEvent, } from './common.js'; import { abiColumn, @@ -86,7 +86,6 @@ import { validateZonefileHash, } from './helpers.js'; import { PgNotifier } from './pg-notifier.js'; -import { SyntheticPoxEventName } from '../pox-helpers.js'; import { BasePgStore, PgSqlClient, PgSqlQuery, connectPostgres } from '@stacks/api-toolkit'; import { getConnectionArgs, getConnectionConfig } from './connection.js'; import * as path from 'path'; @@ -94,6 +93,7 @@ import { PgStoreV2 } from './pg-store-v2.js'; import { ENV } from '../env.js'; import { BlockIdParam } from '../api/routes/v2/schemas.js'; import { PgStoreV3 } from './v3/pg-store-v3.js'; +import { Pox4EventName } from '@stacks/codec'; export const MIGRATIONS_DIR = path.join(REPO_DIR, 'migrations'); @@ -1961,8 +1961,8 @@ export class PgStore extends BasePgStore { }: { limit: number; offset: number; - poxTable: PoxSyntheticEventTable; - }): Promise { + poxTable: Pox4SyntheticEventTable; + }): Promise { return await this.sqlTransaction(async sql => { const cols = poxTable === 'pox4_events' ? POX4_SYNTHETIC_EVENT_COLUMNS : POX_SYNTHETIC_EVENT_COLUMNS; @@ -1984,8 +1984,8 @@ export class PgStore extends BasePgStore { poxTable, }: { txId: string; - poxTable: PoxSyntheticEventTable; - }): Promise> { + poxTable: Pox4SyntheticEventTable; + }): Promise> { return await this.sqlTransaction(async sql => { const dbTx = await this.getTx({ txId, includeUnanchored: true }); if (!dbTx.found) { @@ -2009,8 +2009,8 @@ export class PgStore extends BasePgStore { poxTable, }: { principal: string; - poxTable: PoxSyntheticEventTable; - }): Promise> { + poxTable: Pox4SyntheticEventTable; + }): Promise> { return await this.sqlTransaction(async sql => { const cols = poxTable === 'pox4_events' ? POX4_SYNTHETIC_EVENT_COLUMNS : POX_SYNTHETIC_EVENT_COLUMNS; @@ -2032,7 +2032,7 @@ export class PgStore extends BasePgStore { afterBlockHeight: number; limit: number; offset: number; - poxTable: PoxSyntheticEventTable; + poxTable: Pox4SyntheticEventTable; }): Promise> { return await this.sqlTransaction(async sql => { const queryResults = await sql< @@ -2056,8 +2056,8 @@ export class PgStore extends BasePgStore { AND microblock_canonical = true AND delegate_to = ${args.delegator} AND name IN ( - ${SyntheticPoxEventName.DelegateStx}, - ${SyntheticPoxEventName.RevokeDelegateStx} + ${Pox4EventName.DelegateStx}, + ${Pox4EventName.RevokeDelegateStx} ) AND block_height <= ${args.blockHeight} AND block_height > ${args.afterBlockHeight} @@ -2075,7 +2075,7 @@ export class PgStore extends BasePgStore { stacker, pox_addr, amount_ustx, unlock_burn_height, block_height::integer, tx_id, COUNT(*) OVER()::integer AS total_rows FROM distinct_rows - WHERE name = ${SyntheticPoxEventName.DelegateStx} + WHERE name = ${Pox4EventName.DelegateStx} ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC LIMIT ${args.limit} OFFSET ${args.offset} @@ -2337,20 +2337,16 @@ export class PgStore extends BasePgStore { WHERE canonical = true AND microblock_canonical = true AND stacker = ${stxAddress} AND block_height <= ${blockHeight} AND ( - (name != ${ - SyntheticPoxEventName.HandleUnlock - } AND burnchain_unlock_height >= ${burnBlockHeight}) + (name != ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height >= ${burnBlockHeight}) OR - (name = ${ - SyntheticPoxEventName.HandleUnlock - } AND burnchain_unlock_height < ${burnBlockHeight}) + (name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${burnBlockHeight}) ) ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC LIMIT 1 `; if (pox2EventQuery.length > 0) { const pox2Event = parseDbPoxSyntheticEvent(pox2EventQuery[0]); - if (pox2Event.name === SyntheticPoxEventName.HandleUnlock) { + if (pox2Event.name === Pox4EventName.HandleUnlock) { // on a handle-unlock, set all of the locked stx related property to empty/default lockTxId = ''; locked = 0n; @@ -2359,7 +2355,7 @@ export class PgStore extends BasePgStore { burnchainLockHeight = 0; } else { lockTxId = pox2Event.tx_id; - locked = pox2Event.locked; + locked = BigInt(pox2Event.locked); burnchainUnlockHeight = Number(pox2Event.burnchain_unlock_height); lockHeight = pox2Event.block_height; const blockQuery = await this.getBlockByHeightInternal(sql, lockHeight); @@ -2378,20 +2374,16 @@ export class PgStore extends BasePgStore { WHERE canonical = true AND microblock_canonical = true AND stacker = ${stxAddress} AND block_height <= ${blockHeight} AND ( - (name != ${ - SyntheticPoxEventName.HandleUnlock - } AND burnchain_unlock_height >= ${burnBlockHeight}) + (name != ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height >= ${burnBlockHeight}) OR - (name = ${ - SyntheticPoxEventName.HandleUnlock - } AND burnchain_unlock_height < ${burnBlockHeight}) + (name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${burnBlockHeight}) ) ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC LIMIT 1 `; if (pox3EventQuery.length > 0) { const pox3Event = parseDbPoxSyntheticEvent(pox3EventQuery[0]); - if (pox3Event.name === SyntheticPoxEventName.HandleUnlock) { + if (pox3Event.name === Pox4EventName.HandleUnlock) { // on a handle-unlock, set all of the locked stx related property to empty/default lockTxId = ''; locked = 0n; @@ -2400,7 +2392,7 @@ export class PgStore extends BasePgStore { burnchainLockHeight = 0; } else { lockTxId = pox3Event.tx_id; - locked = pox3Event.locked; + locked = BigInt(pox3Event.locked); burnchainUnlockHeight = Number(pox3Event.burnchain_unlock_height); lockHeight = pox3Event.block_height; const blockQuery = await this.getBlockByHeightInternal(sql, lockHeight); @@ -2421,20 +2413,16 @@ export class PgStore extends BasePgStore { WHERE canonical = true AND microblock_canonical = true AND stacker = ${stxAddress} AND block_height <= ${blockHeight} AND ( - (name != ${ - SyntheticPoxEventName.HandleUnlock - } AND burnchain_unlock_height >= ${burnBlockHeight}) + (name != ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height >= ${burnBlockHeight}) OR - (name = ${ - SyntheticPoxEventName.HandleUnlock - } AND burnchain_unlock_height < ${burnBlockHeight}) + (name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${burnBlockHeight}) ) ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC LIMIT 1 `; if (pox4EventQuery.length > 0) { const pox4Event = parseDbPoxSyntheticEvent(pox4EventQuery[0]); - if (pox4Event.name === SyntheticPoxEventName.HandleUnlock) { + if (pox4Event.name === Pox4EventName.HandleUnlock) { // on a handle-unlock, set all of the locked stx related property to empty/default lockTxId = ''; locked = 0n; @@ -2443,7 +2431,7 @@ export class PgStore extends BasePgStore { burnchainLockHeight = 0; } else { lockTxId = pox4Event.tx_id; - locked = pox4Event.locked; + locked = BigInt(pox4Event.locked); burnchainUnlockHeight = Number(pox4Event.burnchain_unlock_height); lockHeight = pox4Event.block_height; const blockQuery = await this.getBlockByHeightInternal(sql, lockHeight); @@ -4230,15 +4218,15 @@ export class PgStore extends BasePgStore { burnchain_unlock_height <= ${current_burn_height} AND burnchain_unlock_height > ${previous_burn_height} AND name IN ${sql([ - SyntheticPoxEventName.StackStx, - SyntheticPoxEventName.StackIncrease, - SyntheticPoxEventName.StackExtend, - SyntheticPoxEventName.DelegateStackStx, - SyntheticPoxEventName.DelegateStackIncrease, - SyntheticPoxEventName.DelegateStackExtend, + Pox4EventName.StackStx, + Pox4EventName.StackIncrease, + Pox4EventName.StackExtend, + Pox4EventName.DelegateStackStx, + Pox4EventName.DelegateStackIncrease, + Pox4EventName.DelegateStackExtend, ])} ) OR ( - name = ${SyntheticPoxEventName.HandleUnlock} + name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${current_burn_height} AND burnchain_unlock_height >= ${previous_burn_height} ) @@ -4271,17 +4259,17 @@ export class PgStore extends BasePgStore { WHERE canonical = true AND microblock_canonical = true AND block_height <= ${block.block_height} AND ( - ( name != ${SyntheticPoxEventName.HandleUnlock} AND + ( name != ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height >= ${current_burn_height}) OR - ( name = ${SyntheticPoxEventName.HandleUnlock} AND + ( name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${current_burn_height}) ) ORDER BY stacker, block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC `; for (const row of pox2EventQuery) { const pox2Event = parseDbPoxSyntheticEvent(row); - if (pox2Event.name !== SyntheticPoxEventName.HandleUnlock) { + if (pox2Event.name !== Pox4EventName.HandleUnlock) { const unlockEvent: StxLockEventResult = { locked_amount: pox2Event.locked.toString(), unlock_height: Number(pox2Event.burnchain_unlock_height), @@ -4308,15 +4296,15 @@ export class PgStore extends BasePgStore { burnchain_unlock_height <= ${current_burn_height} AND burnchain_unlock_height > ${previous_burn_height} AND name IN ${sql([ - SyntheticPoxEventName.StackStx, - SyntheticPoxEventName.StackIncrease, - SyntheticPoxEventName.StackExtend, - SyntheticPoxEventName.DelegateStackStx, - SyntheticPoxEventName.DelegateStackIncrease, - SyntheticPoxEventName.DelegateStackExtend, + Pox4EventName.StackStx, + Pox4EventName.StackIncrease, + Pox4EventName.StackExtend, + Pox4EventName.DelegateStackStx, + Pox4EventName.DelegateStackIncrease, + Pox4EventName.DelegateStackExtend, ])} ) OR ( - name = ${SyntheticPoxEventName.HandleUnlock} + name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${current_burn_height} AND burnchain_unlock_height >= ${previous_burn_height} ) @@ -4350,17 +4338,17 @@ export class PgStore extends BasePgStore { WHERE canonical = true AND microblock_canonical = true AND block_height <= ${block.block_height} AND ( - ( name != ${SyntheticPoxEventName.HandleUnlock} AND + ( name != ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height >= ${current_burn_height}) OR - ( name = ${SyntheticPoxEventName.HandleUnlock} AND + ( name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${current_burn_height}) ) ORDER BY stacker, block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC `; for (const row of pox3EventQuery) { const pox3Event = parseDbPoxSyntheticEvent(row); - if (pox3Event.name !== SyntheticPoxEventName.HandleUnlock) { + if (pox3Event.name !== Pox4EventName.HandleUnlock) { const unlockEvent: StxLockEventResult = { locked_amount: pox3Event.locked.toString(), unlock_height: Number(pox3Event.burnchain_unlock_height), @@ -4386,15 +4374,15 @@ export class PgStore extends BasePgStore { burnchain_unlock_height <= ${current_burn_height} AND burnchain_unlock_height > ${previous_burn_height} AND name IN ${sql([ - SyntheticPoxEventName.StackStx, - SyntheticPoxEventName.StackIncrease, - SyntheticPoxEventName.StackExtend, - SyntheticPoxEventName.DelegateStackStx, - SyntheticPoxEventName.DelegateStackIncrease, - SyntheticPoxEventName.DelegateStackExtend, + Pox4EventName.StackStx, + Pox4EventName.StackIncrease, + Pox4EventName.StackExtend, + Pox4EventName.DelegateStackStx, + Pox4EventName.DelegateStackIncrease, + Pox4EventName.DelegateStackExtend, ])} ) OR ( - name = ${SyntheticPoxEventName.HandleUnlock} + name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${current_burn_height} AND burnchain_unlock_height >= ${previous_burn_height} ) @@ -4484,7 +4472,7 @@ export class PgStore extends BasePgStore { ( SELECT CASE WHEN burnchain_unlock_height >= (SELECT burn_block_height FROM chain_tip) - AND name != ${SyntheticPoxEventName.HandleUnlock} + AND name != ${Pox4EventName.HandleUnlock} THEN 'locked' ELSE 'unlocked' END diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 4ed3046fa..b4fe43594 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -51,14 +51,14 @@ import { DataStoreAttachmentData, DataStoreAttachmentSubdomainData, DataStoreBnsBlockData, - PoxSyntheticEventInsertValues, + Pox4SyntheticEventInsertValues, DbTxRaw, DbMempoolTxRaw, DbChainTip, NftCustodyInsertValues, DataStoreBnsBlockTxData, - DbPoxSyntheticEvent, - PoxSyntheticEventTable, + DbPox4SyntheticEvent, + Pox4SyntheticEventTable, DbPoxSetSigners, PoxSetSignerValues, PoxCycleInsertValues, @@ -84,7 +84,6 @@ import { PgNotifier } from './pg-notifier.js'; import { MIGRATIONS_DIR, PgStore } from './pg-store.js'; import * as zoneFileParser from 'zone-file'; import { parseResolver, parseZoneFileTxt } from '../event-stream/bns/bns-helpers.js'; -import { SyntheticPoxEventName } from '../pox-helpers.js'; import { PgSqlClient, batchIterate, @@ -98,6 +97,7 @@ import { PgServer, getConnectionArgs, getConnectionConfig } from './connection.j import { BigNumber } from 'bignumber.js'; import { RedisNotifier } from './redis-notifier.js'; import { ENV } from '../env.js'; +import { Pox4EventName } from '@stacks/codec'; const INSERT_BATCH_SIZE = 500; @@ -364,9 +364,9 @@ export class PgWriteStore extends PgStore { q.enqueue(() => this.updateStxEvents(sql, newTxData)); q.enqueue(() => this.updatePrincipalTxs(sql, newTxData)); q.enqueue(() => this.updateSmartContractEvents(sql, newTxData)); - q.enqueue(() => this.updatePoxSyntheticEvents(sql, 'pox2_events', newTxData)); - q.enqueue(() => this.updatePoxSyntheticEvents(sql, 'pox3_events', newTxData)); - q.enqueue(() => this.updatePoxSyntheticEvents(sql, 'pox4_events', newTxData)); + q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox2_events', newTxData)); + q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox3_events', newTxData)); + q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox4_events', newTxData)); q.enqueue(() => this.updateStxLockEvents(sql, newTxData)); q.enqueue(() => this.updateFtEvents(sql, newTxData)); for (const entry of newTxData) { @@ -763,6 +763,7 @@ export class PgWriteStore extends PgStore { pox2Events: entry.pox2Events.map(e => ({ ...e, block_height: blockHeight })), pox3Events: entry.pox3Events.map(e => ({ ...e, block_height: blockHeight })), pox4Events: entry.pox4Events.map(e => ({ ...e, block_height: blockHeight })), + pox5Events: entry.pox5Events.map(e => ({ ...e, block_height: blockHeight })), }); deployedSmartContracts.push(...entry.smartContracts); contractLogEvents.push(...entry.contractLogEvents); @@ -908,20 +909,20 @@ export class PgWriteStore extends PgStore { logger.info('Updated block zero boot data', tablesUpdates); } - async updatePoxSyntheticEvents< - T extends PoxSyntheticEventTable, + async updatePox4SyntheticEvents< + T extends Pox4SyntheticEventTable, Entry extends { tx: DbTx } & ('pox2_events' extends T - ? { pox2Events: DbPoxSyntheticEvent[] } + ? { pox2Events: DbPox4SyntheticEvent[] } : 'pox3_events' extends T - ? { pox3Events: DbPoxSyntheticEvent[] } + ? { pox3Events: DbPox4SyntheticEvent[] } : 'pox4_events' extends T - ? { pox4Events: DbPoxSyntheticEvent[] } + ? { pox4Events: DbPox4SyntheticEvent[] } : never), >(sql: PgSqlClient, poxTable: T, entries: Entry[]) { - const values: PoxSyntheticEventInsertValues[] = []; + const values: Pox4SyntheticEventInsertValues[] = []; for (const entry of entries) { // eslint-disable-next-line no-useless-assignment - let events: DbPoxSyntheticEvent[] | null = null; + let events: DbPox4SyntheticEvent[] | null = null; switch (poxTable) { case 'pox2_events': assert('pox2Events' in entry); @@ -940,7 +941,8 @@ export class PgWriteStore extends PgStore { } const tx = entry.tx; for (const event of events ?? []) { - const value: PoxSyntheticEventInsertValues = { + assert(event.pox_version === 'pox4', 'only pox4 events are supported'); + const value: Pox4SyntheticEventInsertValues = { event_index: event.event_index, tx_id: event.tx_id, tx_index: event.tx_index, @@ -980,12 +982,12 @@ export class PgWriteStore extends PgStore { // Set event-specific columns switch (event.name) { - case SyntheticPoxEventName.HandleUnlock: { + case Pox4EventName.HandleUnlock: { value.first_cycle_locked = event.data.first_cycle_locked.toString(); value.first_unlocked_cycle = event.data.first_unlocked_cycle.toString(); break; } - case SyntheticPoxEventName.StackStx: { + case Pox4EventName.StackStx: { value.lock_period = event.data.lock_period.toString(); value.lock_amount = event.data.lock_amount.toString(); value.start_burn_height = event.data.start_burn_height.toString(); @@ -997,7 +999,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.StackIncrease: { + case Pox4EventName.StackIncrease: { value.increase_by = event.data.increase_by.toString(); value.total_locked = event.data.total_locked.toString(); if (poxTable === 'pox4_events') { @@ -1007,7 +1009,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.StackExtend: { + case Pox4EventName.StackExtend: { value.extend_count = event.data.extend_count.toString(); value.unlock_burn_height = event.data.unlock_burn_height.toString(); if (poxTable === 'pox4_events') { @@ -1017,7 +1019,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.DelegateStx: { + case Pox4EventName.DelegateStx: { value.amount_ustx = event.data.amount_ustx.toString(); value.delegate_to = event.data.delegate_to; value.unlock_burn_height = event.data.unlock_burn_height?.toString() ?? null; @@ -1027,7 +1029,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.DelegateStackStx: { + case Pox4EventName.DelegateStackStx: { value.lock_period = event.data.lock_period.toString(); value.lock_amount = event.data.lock_amount.toString(); value.start_burn_height = event.data.start_burn_height.toString(); @@ -1039,7 +1041,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.DelegateStackIncrease: { + case Pox4EventName.DelegateStackIncrease: { value.increase_by = event.data.increase_by.toString(); value.total_locked = event.data.total_locked.toString(); value.delegator = event.data.delegator; @@ -1049,7 +1051,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.DelegateStackExtend: { + case Pox4EventName.DelegateStackExtend: { value.extend_count = event.data.extend_count.toString(); value.unlock_burn_height = event.data.unlock_burn_height.toString(); value.delegator = event.data.delegator; @@ -1059,7 +1061,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.StackAggregationCommit: { + case Pox4EventName.StackAggregationCommit: { value.reward_cycle = event.data.reward_cycle.toString(); value.amount_ustx = event.data.amount_ustx.toString(); if (poxTable === 'pox4_events') { @@ -1069,7 +1071,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.StackAggregationCommitIndexed: { + case Pox4EventName.StackAggregationCommitIndexed: { value.reward_cycle = event.data.reward_cycle.toString(); value.amount_ustx = event.data.amount_ustx.toString(); if (poxTable === 'pox4_events') { @@ -1079,7 +1081,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.StackAggregationIncrease: { + case Pox4EventName.StackAggregationIncrease: { value.reward_cycle = event.data.reward_cycle.toString(); value.amount_ustx = event.data.amount_ustx.toString(); if (poxTable === 'pox4_events') { @@ -1088,7 +1090,7 @@ export class PgWriteStore extends PgStore { } break; } - case SyntheticPoxEventName.RevokeDelegateStx: { + case Pox4EventName.RevokeDelegateStx: { value.delegate_to = event.data.delegate_to; if (poxTable === 'pox4_events') { value.end_cycle_id = event.data.end_cycle_id?.toString() ?? null; @@ -1098,7 +1100,7 @@ export class PgWriteStore extends PgStore { } default: { throw new Error( - `Unexpected Pox synthetic event name: ${(event as DbPoxSyntheticEvent).name}` + `Unexpected Pox synthetic event name: ${(event as DbPox4SyntheticEvent).name}` ); } } @@ -2923,9 +2925,9 @@ export class PgWriteStore extends PgStore { q.enqueue(() => this.updateStxEvents(sql, txs)); q.enqueue(() => this.updatePrincipalTxs(sql, txs)); q.enqueue(() => this.updateSmartContractEvents(sql, txs)); - q.enqueue(() => this.updatePoxSyntheticEvents(sql, 'pox2_events', txs)); - q.enqueue(() => this.updatePoxSyntheticEvents(sql, 'pox3_events', txs)); - q.enqueue(() => this.updatePoxSyntheticEvents(sql, 'pox4_events', txs)); + q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox2_events', txs)); + q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox3_events', txs)); + q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox4_events', txs)); q.enqueue(() => this.updateStxLockEvents(sql, txs)); q.enqueue(() => this.updateFtEvents(sql, txs)); for (const entry of txs) { diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index 3580269a6..7312a936b 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -22,7 +22,7 @@ import { DataStoreTxEventData, DbMicroblock, DataStoreAttachmentData, - DbPoxSyntheticEvent, + DbPox4SyntheticEvent, DbTxStatus, DbPoxSetSigners, DbBurnBlockPoxTx, @@ -36,7 +36,12 @@ import { isPoxPrintEvent, newCoreNoreBlockEventCounts, } from './reader.js'; -import { decodeClarityValue, decodeTransaction, TxPayloadTypeID } from '@stacks/codec'; +import { + decodeClarityValue, + decodePoxSyntheticEvent, + decodeTransaction, + TxPayloadTypeID, +} from '@stacks/codec'; import type { ClarityValueBuffer, ClarityValueStringAscii, ClarityValueTuple } from '@stacks/codec'; import { BnsContractIdentifier } from './bns/bns-constants.js'; import { @@ -51,10 +56,9 @@ import { getTxDbStatus, } from '../datastore/helpers.js'; import { handleBnsImport } from '../import-v1/index.js'; -import { decodePoxSyntheticPrintEvent } from './pox-event-parsing.js'; import { hexToBuffer, isProdEnv, logger, PINO_LOGGER_CONFIG, stopwatch } from '@stacks/api-toolkit'; import { ENV } from '../env.js'; -import { POX_2_CONTRACT_NAME, POX_3_CONTRACT_NAME, POX_4_CONTRACT_NAME } from '../pox-helpers.js'; +import { POX_2_CONTRACT_NAME, POX_3_CONTRACT_NAME, POX_4_CONTRACT_NAME } from './pox-constants.js'; import { DropMempoolTxMessage, NewBlockEvent, @@ -277,6 +281,7 @@ function parseDataStoreTxEventData( pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }; switch (tx.parsed_tx.payload.type_id) { case TxPayloadTypeID.VersionedSmartContract: @@ -313,7 +318,7 @@ function parseDataStoreTxEventData( return dbTx; }); - const poxEventLogs: Map = new Map(); + const poxEventLogs: Map = new Map(); for (const event of events) { if (!event.committed) { @@ -364,39 +369,37 @@ function parseDataStoreTxEventData( dbTx.contractLogEvents.push(entry); if (isPoxPrintEvent(event)) { - const network = getChainIDNetwork(chainId) === 'mainnet' ? 'mainnet' : 'testnet'; + const network = getChainIDNetwork(chainId); const [, contractName] = event.contract_event.contract_identifier.split('.'); - // pox-1 is handled in custom node events - const processSyntheticEvent = [ - POX_2_CONTRACT_NAME, - POX_3_CONTRACT_NAME, - POX_4_CONTRACT_NAME, - ].includes(contractName); - if (processSyntheticEvent) { - const poxEventData = decodePoxSyntheticPrintEvent( - event.contract_event.raw_value, - network - ); - if (poxEventData !== null) { - logger.debug(`Synthetic pox event data for ${contractName}:`, poxEventData); - const dbPoxEvent: DbPoxSyntheticEvent = { - ...dbEvent, - ...poxEventData, - }; - poxEventLogs.set(dbPoxEvent, entry); - switch (contractName) { - case POX_2_CONTRACT_NAME: { - dbTx.pox2Events.push(dbPoxEvent); - break; - } - case POX_3_CONTRACT_NAME: { - dbTx.pox3Events.push(dbPoxEvent); - break; - } - case POX_4_CONTRACT_NAME: { - dbTx.pox4Events.push(dbPoxEvent); - break; + const poxEvent = decodePoxSyntheticEvent(event.contract_event.raw_value, network); + if (poxEvent) { + logger.debug(`Decoded pox event for ${contractName}:`, poxEvent); + switch (poxEvent.pox_version) { + case 'pox4': { + const dbPoxEvent: DbPox4SyntheticEvent = { + ...dbEvent, + ...poxEvent, + }; + poxEventLogs.set(dbPoxEvent, entry); + switch (contractName) { + case POX_2_CONTRACT_NAME: { + dbTx.pox2Events.push(dbPoxEvent); + break; + } + case POX_3_CONTRACT_NAME: { + dbTx.pox3Events.push(dbPoxEvent); + break; + } + case POX_4_CONTRACT_NAME: { + dbTx.pox4Events.push(dbPoxEvent); + break; + } } + break; + } + case 'pox5': { + // dbTx.pox5Events.push(dbPoxEvent); + break; } } } diff --git a/src/pox-helpers.ts b/src/event-stream/pox-constants.ts similarity index 58% rename from src/pox-helpers.ts rename to src/event-stream/pox-constants.ts index f35bd444f..37f4e2d33 100644 --- a/src/pox-helpers.ts +++ b/src/event-stream/pox-constants.ts @@ -1,19 +1,3 @@ -/** Names for synthetic events generated for the pox-2, pox-3, and pox-4 contracts */ -export enum SyntheticPoxEventName { - HandleUnlock = 'handle-unlock', - StackStx = 'stack-stx', - StackIncrease = 'stack-increase', - StackExtend = 'stack-extend', - DelegateStx = 'delegate-stx', - DelegateStackStx = 'delegate-stack-stx', - DelegateStackIncrease = 'delegate-stack-increase', - DelegateStackExtend = 'delegate-stack-extend', - StackAggregationCommit = 'stack-aggregation-commit', - StackAggregationCommitIndexed = 'stack-aggregation-commit-indexed', - StackAggregationIncrease = 'stack-aggregation-increase', - RevokeDelegateStx = 'revoke-delegate-stx', // Only guaranteed to be present in pox-4 -} - const BOOT_ADDR_MAINNET = 'SP000000000000000000002Q6VF78'; const BOOT_ADDR_TESTNET = 'ST000000000000000000002AMW42H'; @@ -21,6 +5,7 @@ const POX_1_CONTRACT_NAME = 'pox'; export const POX_2_CONTRACT_NAME = 'pox-2'; export const POX_3_CONTRACT_NAME = 'pox-3'; export const POX_4_CONTRACT_NAME = 'pox-4'; +export const POX_5_CONTRACT_NAME = 'pox-5'; export const PoxContractIdentifier = { pox1: { @@ -39,6 +24,10 @@ export const PoxContractIdentifier = { mainnet: `${BOOT_ADDR_MAINNET}.${POX_4_CONTRACT_NAME}`, testnet: `${BOOT_ADDR_TESTNET}.${POX_4_CONTRACT_NAME}`, }, + pox5: { + mainnet: `${BOOT_ADDR_MAINNET}.${POX_5_CONTRACT_NAME}`, + testnet: `${BOOT_ADDR_TESTNET}.${POX_5_CONTRACT_NAME}`, + }, } as const; export const PoxContractIdentifiers = Object.values(PoxContractIdentifier).flatMap( diff --git a/src/event-stream/pox-event-parsing.ts b/src/event-stream/pox-event-parsing.ts deleted file mode 100644 index 5411b0124..000000000 --- a/src/event-stream/pox-event-parsing.ts +++ /dev/null @@ -1,512 +0,0 @@ -import { - DbPoxSyntheticBaseEventData, - DbPoxSyntheticDelegateStackExtendEvent, - DbPoxSyntheticDelegateStackIncreaseEvent, - DbPoxSyntheticDelegateStackStxEvent, - DbPoxSyntheticDelegateStxEvent, - DbPoxSyntheticEventData, - DbPoxSyntheticHandleUnlockEvent, - DbPoxSyntheticRevokeDelegateStxEvent, - DbPoxSyntheticStackAggregationCommitEvent, - DbPoxSyntheticStackAggregationCommitIndexedEvent, - DbPoxSyntheticStackAggregationIncreaseEvent, - DbPoxSyntheticStackExtendEvent, - DbPoxSyntheticStackIncreaseEvent, - DbPoxSyntheticStackStxEvent, -} from '../datastore/common.js'; -import { ClarityTypeID, decodeClarityValue } from '@stacks/codec'; -import type { - ClarityValue, - ClarityValueAbstract, - ClarityValueBuffer, - ClarityValueOptional, - ClarityValueOptionalNone, - ClarityValueOptionalSome, - ClarityValuePrincipalContract, - ClarityValuePrincipalStandard, - ClarityValueResponse, - ClarityValueStringAscii, - ClarityValueTuple, - ClarityValueUInt, -} from '@stacks/codec'; -import { poxAddressToBtcAddress } from '@stacks/stacking'; -import { SyntheticPoxEventName } from '../pox-helpers.js'; -import { bufferToHex, coerceToBuffer, logger } from '@stacks/api-toolkit'; - -function tryClarityPoxAddressToBtcAddress( - poxAddr: - | PoxSyntheticEventAddr - | ClarityValueOptionalSome - | ClarityValueOptionalNone, - network: 'mainnet' | 'testnet' | 'devnet' | 'mocknet' -): { btcAddr: string | null; raw: Buffer } { - let btcAddr: string | null = null; - if (poxAddr.type_id === ClarityTypeID.OptionalNone) { - return { - btcAddr, - raw: Buffer.alloc(0), - }; - } - if (poxAddr.type_id === ClarityTypeID.OptionalSome) { - poxAddr = poxAddr.value; - } - try { - btcAddr = poxAddressToBtcAddress( - coerceToBuffer(poxAddr.data.version.buffer)[0], - coerceToBuffer(poxAddr.data.hashbytes.buffer), - network - ); - } catch (e) { - logger.debug( - `Error encoding PoX address version: ${poxAddr.data.version.buffer}, hashbytes: ${poxAddr.data.hashbytes.buffer} to bitcoin address: ${e}` - ); - btcAddr = null; - } - return { - btcAddr, - raw: Buffer.concat([ - coerceToBuffer(poxAddr.data.version.buffer), - coerceToBuffer(poxAddr.data.hashbytes.buffer), - ]), - }; -} - -type PoxSyntheticEventAddr = ClarityValueTuple<{ - hashbytes: ClarityValueBuffer; - version: ClarityValueBuffer; -}>; - -type PoXSyntheticEventData = ClarityValueTuple<{ - name: ClarityValueStringAscii; - balance: ClarityValueUInt; - stacker: ClarityValuePrincipalStandard | ClarityValuePrincipalContract; - locked: ClarityValueUInt; - 'burnchain-unlock-height': ClarityValueUInt; - data: ClarityValueTuple; -}>; - -interface PoxSyntheticPrintEventTypes { - [SyntheticPoxEventName.HandleUnlock]: { - 'first-cycle-locked': ClarityValueUInt; - 'first-unlocked-cycle': ClarityValueUInt; - }; - [SyntheticPoxEventName.StackStx]: { - 'lock-amount': ClarityValueUInt; - 'lock-period': ClarityValueUInt; - 'pox-addr': PoxSyntheticEventAddr; - 'start-burn-height': ClarityValueUInt; - 'unlock-burn-height': ClarityValueUInt; - 'signer-key'?: ClarityValueBuffer | ClarityValueOptionalNone; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; - }; - [SyntheticPoxEventName.StackIncrease]: { - 'increase-by': ClarityValueUInt; - 'total-locked': ClarityValueUInt; - 'signer-key'?: ClarityValueBuffer | ClarityValueOptionalNone; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; - }; - [SyntheticPoxEventName.StackExtend]: { - 'extend-count': ClarityValueUInt; - 'unlock-burn-height': ClarityValueUInt; - 'pox-addr': PoxSyntheticEventAddr; - 'signer-key'?: ClarityValueBuffer | ClarityValueOptionalNone; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; - }; - [SyntheticPoxEventName.DelegateStx]: { - 'amount-ustx': ClarityValueUInt; - 'delegate-to': ClarityValuePrincipalStandard | ClarityValuePrincipalContract; - 'unlock-burn-height': ClarityValueOptionalSome | ClarityValueOptionalNone; - 'pox-addr': PoxSyntheticEventAddr | ClarityValueOptionalNone; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; // TODO: add this to core node - }; - [SyntheticPoxEventName.DelegateStackStx]: { - 'lock-amount': ClarityValueUInt; - 'unlock-burn-height': ClarityValueUInt; - 'pox-addr': PoxSyntheticEventAddr; - 'start-burn-height': ClarityValueUInt; - 'lock-period': ClarityValueUInt; - delegator: ClarityValuePrincipalStandard | ClarityValuePrincipalContract; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; - }; - [SyntheticPoxEventName.DelegateStackIncrease]: { - 'pox-addr': PoxSyntheticEventAddr; - 'increase-by': ClarityValueUInt; - 'total-locked': ClarityValueUInt; - delegator: ClarityValuePrincipalStandard | ClarityValuePrincipalContract; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; // TODO: add this to core node - }; - [SyntheticPoxEventName.DelegateStackExtend]: { - 'pox-addr': PoxSyntheticEventAddr; - 'unlock-burn-height': ClarityValueUInt; - 'extend-count': ClarityValueUInt; - delegator: ClarityValuePrincipalStandard | ClarityValuePrincipalContract; - 'signer-key'?: ClarityValueBuffer | ClarityValueOptionalNone; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; // TODO: add this to core node - }; - [SyntheticPoxEventName.StackAggregationCommit]: { - 'pox-addr': PoxSyntheticEventAddr; - 'reward-cycle': ClarityValueUInt; - 'amount-ustx': ClarityValueUInt; - 'signer-key'?: ClarityValueBuffer | ClarityValueOptionalNone; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; - }; - [SyntheticPoxEventName.StackAggregationCommitIndexed]: { - 'pox-addr': PoxSyntheticEventAddr; - 'reward-cycle': ClarityValueUInt; - 'amount-ustx': ClarityValueUInt; - 'signer-key'?: ClarityValueBuffer | ClarityValueOptionalNone; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; - }; - [SyntheticPoxEventName.StackAggregationIncrease]: { - 'pox-addr': PoxSyntheticEventAddr; - 'reward-cycle': ClarityValueUInt; - 'amount-ustx': ClarityValueUInt; - 'reward-cycle-index'?: ClarityValueUInt; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; - }; - [SyntheticPoxEventName.RevokeDelegateStx]: { - 'delegate-to': ClarityValuePrincipalStandard | ClarityValuePrincipalContract; - 'end-cycle-id'?: ClarityValueOptional; - 'start-cycle-id'?: ClarityValueUInt; - }; -} - -function clarityPrincipalToFullAddress( - principal: ClarityValuePrincipalStandard | ClarityValuePrincipalContract -): string { - if (principal.type_id === ClarityTypeID.PrincipalStandard) { - return principal.address; - } else if (principal.type_id === ClarityTypeID.PrincipalContract) { - return `${principal.address}.${principal.contract_name}`; - } - throw new Error( - `Unexpected Clarity value type for principal: ${(principal as ClarityValue).type_id}` - ); -} - -// TODO: this and the logic referencing it can be removed once the "stale" data issue is fixed in -// https://github.com/stacks-network/stacks-blockchain/pull/3318 -const PATCH_EVENT_BALANCES = true; - -export function decodePoxSyntheticPrintEvent( - rawClarityData: string, - network: 'mainnet' | 'testnet' | 'devnet' | 'mocknet' -): DbPoxSyntheticEventData | null { - const decoded = decodeClarityValue(rawClarityData); - if (decoded.type_id === ClarityTypeID.ResponseError) { - logger.info(`Received ResponseError when decoding Pox synthetic print event: ${decoded.repr}`); - return null; - } - if (decoded.type_id !== ClarityTypeID.ResponseOk) { - const valCommon: ClarityValueAbstract = decoded; - throw new Error( - `Unexpected PoX synthetic event Clarity type ID, expected ResponseOk, got ${valCommon.type_id}: ${valCommon.repr}` - ); - } - if (decoded.value.type_id !== ClarityTypeID.Tuple) { - throw new Error( - `Unexpected PoX synthetic event Clarity type ID, expected Tuple, got ${decoded.value.type_id}` - ); - } - const opData = (decoded.value as PoXSyntheticEventData).data; - - const baseEventData: DbPoxSyntheticBaseEventData = { - stacker: clarityPrincipalToFullAddress(opData.stacker), - locked: BigInt(opData.locked.value), - balance: BigInt(opData.balance.value), - burnchain_unlock_height: BigInt(opData['burnchain-unlock-height'].value), - pox_addr: null, - pox_addr_raw: null, - }; - - const eventName = opData.name.data as keyof PoxSyntheticPrintEventTypes; - if (opData.name.type_id !== ClarityTypeID.StringAscii) { - throw new Error( - `Unexpected PoX synthetic event name type, expected StringAscii, got ${opData.name.type_id}` - ); - } - - const eventData = opData.data.data; - if (opData.data.type_id !== ClarityTypeID.Tuple) { - throw new Error( - `Unexpected PoX synthetic event data payload type, expected Tuple, got ${opData.data.type_id}` - ); - } - - if ('pox-addr' in eventData) { - const eventPoxAddr = eventData['pox-addr'] as - | PoxSyntheticEventAddr - | ClarityValueOptionalSome - | ClarityValueOptionalNone; - const encodedArr = tryClarityPoxAddressToBtcAddress(eventPoxAddr, network); - baseEventData.pox_addr = encodedArr.btcAddr; - baseEventData.pox_addr_raw = bufferToHex(encodedArr.raw); - } - - switch (eventName) { - case SyntheticPoxEventName.HandleUnlock: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticHandleUnlockEvent = { - ...baseEventData, - name: eventName, - data: { - first_cycle_locked: BigInt(d['first-cycle-locked'].value), - first_unlocked_cycle: BigInt(d['first-unlocked-cycle'].value), - }, - }; - if (PATCH_EVENT_BALANCES) { - // Note: `burnchain_unlock_height` is correct for `handle-unlock`, and does not need "patched" like the others - parsedData.balance += parsedData.locked; - } - return parsedData; - } - case SyntheticPoxEventName.StackStx: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticStackStxEvent = { - ...baseEventData, - name: eventName, - data: { - lock_amount: BigInt(d['lock-amount'].value), - lock_period: BigInt(d['lock-period'].value), - start_burn_height: BigInt(d['start-burn-height'].value), - unlock_burn_height: BigInt(d['unlock-burn-height'].value), - signer_key: - d['signer-key']?.type_id === ClarityTypeID.Buffer ? d['signer-key'].buffer : null, - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - if (PATCH_EVENT_BALANCES) { - parsedData.burnchain_unlock_height = parsedData.data.unlock_burn_height; - parsedData.balance -= parsedData.data.lock_amount; - parsedData.locked = parsedData.data.lock_amount; - } - return parsedData; - } - case SyntheticPoxEventName.StackIncrease: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticStackIncreaseEvent = { - ...baseEventData, - name: eventName, - data: { - increase_by: BigInt(d['increase-by'].value), - total_locked: BigInt(d['total-locked'].value), - signer_key: - d['signer-key']?.type_id === ClarityTypeID.Buffer ? d['signer-key'].buffer : null, - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - if (PATCH_EVENT_BALANCES) { - parsedData.balance -= parsedData.data.increase_by; - parsedData.locked += parsedData.data.increase_by; - } - return parsedData; - } - case SyntheticPoxEventName.StackExtend: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticStackExtendEvent = { - ...baseEventData, - name: eventName, - data: { - extend_count: BigInt(d['extend-count'].value), - unlock_burn_height: BigInt(d['unlock-burn-height'].value), - signer_key: - d['signer-key']?.type_id === ClarityTypeID.Buffer ? d['signer-key'].buffer : null, - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - if (PATCH_EVENT_BALANCES) { - parsedData.burnchain_unlock_height = parsedData.data.unlock_burn_height; - } - return parsedData; - } - case SyntheticPoxEventName.DelegateStx: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticDelegateStxEvent = { - ...baseEventData, - name: eventName, - data: { - amount_ustx: BigInt(d['amount-ustx'].value), - delegate_to: clarityPrincipalToFullAddress(d['delegate-to']), - unlock_burn_height: - d['unlock-burn-height'].type_id === ClarityTypeID.OptionalSome - ? BigInt(d['unlock-burn-height'].value.value) - : null, - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - if (PATCH_EVENT_BALANCES) { - if (parsedData.data.unlock_burn_height) { - parsedData.burnchain_unlock_height = parsedData.data.unlock_burn_height; - } - } - return parsedData; - } - case SyntheticPoxEventName.DelegateStackStx: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticDelegateStackStxEvent = { - ...baseEventData, - name: eventName, - data: { - lock_amount: BigInt(d['lock-amount'].value), - unlock_burn_height: BigInt(d['unlock-burn-height'].value), - start_burn_height: BigInt(d['start-burn-height'].value), - lock_period: BigInt(d['lock-period'].value), - delegator: clarityPrincipalToFullAddress(d['delegator']), - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - if (PATCH_EVENT_BALANCES) { - parsedData.burnchain_unlock_height = parsedData.data.unlock_burn_height; - parsedData.balance -= parsedData.data.lock_amount; - parsedData.locked = parsedData.data.lock_amount; - } - return parsedData; - } - case SyntheticPoxEventName.DelegateStackIncrease: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticDelegateStackIncreaseEvent = { - ...baseEventData, - name: eventName, - data: { - increase_by: BigInt(d['increase-by'].value), - total_locked: BigInt(d['total-locked'].value), - delegator: clarityPrincipalToFullAddress(d['delegator']), - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - if (PATCH_EVENT_BALANCES) { - parsedData.balance -= parsedData.data.increase_by; - parsedData.locked += parsedData.data.increase_by; - } - return parsedData; - } - case SyntheticPoxEventName.DelegateStackExtend: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticDelegateStackExtendEvent = { - ...baseEventData, - name: eventName, - data: { - unlock_burn_height: BigInt(d['unlock-burn-height'].value), - extend_count: BigInt(d['extend-count'].value), - delegator: clarityPrincipalToFullAddress(d['delegator']), - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - if (PATCH_EVENT_BALANCES) { - parsedData.burnchain_unlock_height = parsedData.data.unlock_burn_height; - } - return parsedData; - } - case SyntheticPoxEventName.StackAggregationCommit: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticStackAggregationCommitEvent = { - ...baseEventData, - name: eventName, - data: { - reward_cycle: BigInt(d['reward-cycle'].value), - amount_ustx: BigInt(d['amount-ustx'].value), - signer_key: - d['signer-key']?.type_id === ClarityTypeID.Buffer ? d['signer-key'].buffer : null, - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - return parsedData; - } - case SyntheticPoxEventName.StackAggregationCommitIndexed: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticStackAggregationCommitIndexedEvent = { - ...baseEventData, - name: eventName, - data: { - reward_cycle: BigInt(d['reward-cycle'].value), - amount_ustx: BigInt(d['amount-ustx'].value), - signer_key: - d['signer-key']?.type_id === ClarityTypeID.Buffer ? d['signer-key'].buffer : null, - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - return parsedData; - } - case SyntheticPoxEventName.StackAggregationIncrease: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticStackAggregationIncreaseEvent = { - ...baseEventData, - name: eventName, - data: { - reward_cycle: BigInt(d['reward-cycle'].value), - amount_ustx: BigInt(d['amount-ustx'].value), - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - return parsedData; - } - case SyntheticPoxEventName.RevokeDelegateStx: { - const d = eventData as PoxSyntheticPrintEventTypes[typeof eventName]; - const parsedData: DbPoxSyntheticRevokeDelegateStxEvent = { - ...baseEventData, - name: eventName, - data: { - delegate_to: clarityPrincipalToFullAddress(d['delegate-to']), - end_cycle_id: - d['end-cycle-id']?.type_id === ClarityTypeID.OptionalSome - ? BigInt(d['end-cycle-id'].value.value) - : null, - start_cycle_id: d['start-cycle-id']?.value ? BigInt(d['start-cycle-id'].value) : null, - }, - }; - return parsedData; - } - default: - throw new Error(`Unexpected PoX synthetic event data name: ${opData.name.data}`); - } -} diff --git a/src/event-stream/reader.ts b/src/event-stream/reader.ts index 3068b0752..50b9a9515 100644 --- a/src/event-stream/reader.ts +++ b/src/event-stream/reader.ts @@ -4,10 +4,12 @@ import { ClarityTypeID, decodeClarityValue, decodeClarityValueList, + decodePoxSyntheticEvent, decodeStacksAddress, decodeTransaction, PostConditionAuthFlag, PostConditionModeID, + Pox4EventName, PrincipalTypeID, TransactionVersion, TxPayloadTypeID, @@ -21,12 +23,10 @@ import type { ClarityValueUInt, DecodedTxResult, ClarityValueBuffer, + Pox4EventStackStx, + Pox4EventDelegateStx, } from '@stacks/codec'; -import { - DbMicroblockPartial, - DbPoxSyntheticDelegateStxEvent, - DbPoxSyntheticStackStxEvent, -} from '../datastore/common.js'; +import { DbMicroblockPartial } from '../datastore/common.js'; import { NotImplementedError } from '../errors.js'; import { getEnumDescription, @@ -52,8 +52,7 @@ import { } from '@stacks/transactions'; import { poxAddressToTuple } from '@stacks/stacking'; import { c32ToB58 } from 'c32check'; -import { decodePoxSyntheticPrintEvent } from './pox-event-parsing.js'; -import { PoxContractIdentifiers, SyntheticPoxEventName } from '../pox-helpers.js'; +import { PoxContractIdentifiers } from './pox-constants.js'; import { bufferToHex, hexToBuffer, logger } from '@stacks/api-toolkit'; import { hexToBytes } from '@stacks/common'; import { @@ -349,7 +348,7 @@ function createTransactionFromCoreBtcStxLockEvent( txResult: string, txId: string, /** also pox-3 compatible */ - stxStacksPox2Event: DbPoxSyntheticStackStxEvent | undefined + stxStacksPox2Event: Pox4EventStackStx | undefined ): DecodedTxResult { const resultCv = decodeClarityValue< ClarityValueResponse< @@ -522,7 +521,7 @@ function createTransactionFromCoreBtcStxLockEventPox4( function createTransactionFromCoreBtcDelegateStxEventPox4( chainId: ChainID, contractEvent: NewBlockContractEvent, - decodedEvent: DbPoxSyntheticDelegateStxEvent, + decodedEvent: Pox4EventDelegateStx, burnOpData: BurnchainOpDelegateStx, txResult: string, txId: string @@ -608,7 +607,7 @@ function createTransactionFromCoreBtcDelegateStxEventPox4( function createTransactionFromCoreBtcDelegateStxEvent( chainId: ChainID, contractEvent: NewBlockContractEvent, - decodedEvent: DbPoxSyntheticDelegateStxEvent, + decodedEvent: Pox4EventDelegateStx, txResult: string, txId: string ): DecodedTxResult { @@ -789,6 +788,8 @@ export function parseMessageTransaction( let rawTx: DecodedTxResult; let txSender: string; let sponsorAddress: string | undefined = undefined; + + // BTC transactions if (coreTx.raw_tx === '0x00') { const events = allEvents.filter(event => event.txid === coreTx.txid); if (events.length === 0) { @@ -819,7 +820,7 @@ export function parseMessageTransaction( ) .map(e => { const network = getChainIDNetwork(chainId); - const decodedEvent = decodePoxSyntheticPrintEvent(e.contract_event.raw_value, network); + const decodedEvent = decodePoxSyntheticEvent(e.contract_event.raw_value, network); if (decodedEvent) { return { contractEvent: e, @@ -856,7 +857,7 @@ export function parseMessageTransaction( txSender = burnOpData.sender.address; } else if (stxLockEvent) { const stxStacksPoxEvent = - poxEvent?.decodedEvent.name === SyntheticPoxEventName.StackStx + poxEvent?.decodedEvent.name === Pox4EventName.StackStx ? poxEvent.decodedEvent : undefined; rawTx = createTransactionFromCoreBtcStxLockEvent( @@ -870,7 +871,7 @@ export function parseMessageTransaction( txSender = stxLockEvent.stx_lock_event.locked_address; } else if ( poxEvent && - poxEvent.decodedEvent.name === SyntheticPoxEventName.DelegateStx && + poxEvent.decodedEvent.name === Pox4EventName.DelegateStx && poxEvent.contractEvent.contract_event.contract_identifier?.split('.')?.[1] === 'pox-4' && coreTx.burnchain_op && 'delegate_stx' in coreTx.burnchain_op @@ -884,7 +885,7 @@ export function parseMessageTransaction( coreTx.txid ); txSender = coreTx.burnchain_op.delegate_stx.sender.address; - } else if (poxEvent && poxEvent.decodedEvent.name === SyntheticPoxEventName.DelegateStx) { + } else if (poxEvent && poxEvent.decodedEvent.name === Pox4EventName.DelegateStx) { rawTx = createTransactionFromCoreBtcDelegateStxEvent( chainId, poxEvent.contractEvent, diff --git a/tests/krypton/pox-4-burnchain-delegate-stx/pox-4-burnchain-delegate-stx.test.ts b/tests/krypton/pox-4-burnchain-delegate-stx/pox-4-burnchain-delegate-stx.test.ts index bfa868fbf..ed5a4ccc3 100644 --- a/tests/krypton/pox-4-burnchain-delegate-stx/pox-4-burnchain-delegate-stx.test.ts +++ b/tests/krypton/pox-4-burnchain-delegate-stx/pox-4-burnchain-delegate-stx.test.ts @@ -12,7 +12,7 @@ import { BootContractAddress } from '../../../src/helpers.ts'; import * as btc from 'bitcoinjs-lib'; import { b58ToC32, c32ToB58 } from 'c32check'; import supertest from 'supertest'; -import { PoxContractIdentifier } from '../../../src/pox-helpers.ts'; +import { PoxContractIdentifier } from '../../../src/event-stream/pox-constants.ts'; import { decodeClarityValue } from '@stacks/codec'; import type { ClarityValueUInt } from '@stacks/codec'; import { decodeBtcAddress, poxAddressToBtcAddress } from '@stacks/stacking'; From 3a4e3d57556a613cc8a306f550e455f932178d2e Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Thu, 21 May 2026 15:27:20 -0600 Subject: [PATCH 03/31] table migration --- migrations/1779392293803_pox5-events.ts | 69 +++++++++++++++++++ src/datastore/common.ts | 10 ++- src/datastore/pg-write-store.ts | 29 ++++++++ tests/api/cache/cache-control.test.ts | 21 +++--- tests/api/datastore/datastore.test.ts | 29 ++++++++ tests/api/datastore/other.test.ts | 1 + tests/api/mempool/mempool.test.ts | 2 + tests/api/microblocks/microblock.test.ts | 3 + tests/api/principal/address.test.ts | 6 ++ .../smart-contracts/smart-contract.test.ts | 6 ++ tests/api/test-builders.ts | 1 + tests/api/transactions/search.test.ts | 4 ++ tests/api/transactions/tx.test.ts | 17 +++++ 13 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 migrations/1779392293803_pox5-events.ts diff --git a/migrations/1779392293803_pox5-events.ts b/migrations/1779392293803_pox5-events.ts new file mode 100644 index 000000000..ee45b5d10 --- /dev/null +++ b/migrations/1779392293803_pox5-events.ts @@ -0,0 +1,69 @@ +import type { MigrationBuilder } from 'node-pg-migrate'; + +export const up = (pgm: MigrationBuilder) => { + pgm.createTable('pox5_events', { + id: { + type: 'bigserial', + primaryKey: true, + }, + event_index: { + type: 'integer', + notNull: true, + }, + tx_id: { + notNull: true, + type: 'bytea', + }, + tx_index: { + type: 'smallint', + notNull: true, + }, + block_height: { + type: 'integer', + notNull: true, + }, + index_block_hash: { + type: 'bytea', + notNull: true, + }, + parent_index_block_hash: { + type: 'bytea', + notNull: true, + }, + microblock_hash: { + type: 'bytea', + notNull: true, + }, + microblock_sequence: { + type: 'integer', + notNull: true, + }, + microblock_canonical: { + type: 'boolean', + notNull: true, + }, + canonical: { + type: 'boolean', + notNull: true, + }, + data: { + type: 'jsonb', + notNull: true, + } + }); + + pgm.createIndex('pox5_events', [ + { name: 'block_height', sort: 'DESC' }, + { name: 'microblock_sequence', sort: 'DESC' }, + { name: 'tx_index', sort: 'DESC' }, + { name: 'event_index', sort: 'DESC' }, + ]); + pgm.createIndex('pox5_events', 'tx_id'); + pgm.createIndex('pox5_events', ['index_block_hash', 'canonical']); + pgm.createIndex('pox5_events', 'microblock_hash'); +} + +export const down = (pgm: MigrationBuilder) => { + pgm.dropTable('pox5_events'); +} + diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 08b917076..b304f58d2 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1,4 +1,4 @@ -import { Pox4Event } from '@stacks/codec'; +import { Pox4Event, Pox5Event } from '@stacks/codec'; import { Block } from '../api/schemas/v1/entities/block.js'; import { PgBytea, PgJsonb, PgNumeric } from '@stacks/api-toolkit'; @@ -392,6 +392,8 @@ export type Pox4SyntheticEventTable = 'pox2_events' | 'pox3_events' | 'pox4_even export type DbPox4SyntheticEvent = DbEventBase & Pox4Event; +export type DbPox5SyntheticEvent = DbEventBase & Pox5Event; + export interface DbPoxStacker { stacker: string; pox_addr?: string; @@ -527,7 +529,7 @@ export interface DataStoreTxEventData { pox2Events: DbPox4SyntheticEvent[]; pox3Events: DbPox4SyntheticEvent[]; pox4Events: DbPox4SyntheticEvent[]; - pox5Events: DbPox4SyntheticEvent[]; + pox5Events: DbPox5SyntheticEvent[]; } export interface DataStoreAttachmentData { @@ -1375,6 +1377,10 @@ export interface Pox5SyntheticEventInsertValues { index_block_hash: PgBytea; parent_index_block_hash: PgBytea; microblock_hash: PgBytea; + microblock_sequence: number; + microblock_canonical: boolean; + canonical: boolean; + data: PgJsonb; } export interface NftEventInsertValues { diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index b4fe43594..60d3cb7d1 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -64,6 +64,7 @@ import { PoxCycleInsertValues, DbAssetEventTypeId, DbBurnBlockPoxTx, + Pox5SyntheticEventInsertValues, } from './common.js'; import { BLOCK_COLUMNS, @@ -367,6 +368,7 @@ export class PgWriteStore extends PgStore { q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox2_events', newTxData)); q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox3_events', newTxData)); q.enqueue(() => this.updatePox4SyntheticEvents(sql, 'pox4_events', newTxData)); + q.enqueue(() => this.insertPox5SyntheticEvents(sql, newTxData)); q.enqueue(() => this.updateStxLockEvents(sql, newTxData)); q.enqueue(() => this.updateFtEvents(sql, newTxData)); for (const entry of newTxData) { @@ -501,6 +503,33 @@ export class PgWriteStore extends PgStore { return new Set(); } + private async insertPox5SyntheticEvents(sql: PgSqlClient, txs: DataStoreTxEventData[]) { + const values: Pox5SyntheticEventInsertValues[] = []; + for (const tx of txs) { + if (tx.pox5Events.length === 0) continue; + values.push( + ...tx.pox5Events.map(e => ({ + event_index: e.event_index, + tx_id: e.tx_id, + tx_index: e.tx_index, + block_height: e.block_height, + index_block_hash: tx.tx.index_block_hash, + parent_index_block_hash: tx.tx.parent_index_block_hash, + microblock_hash: tx.tx.microblock_hash, + microblock_sequence: tx.tx.microblock_sequence, + microblock_canonical: tx.tx.microblock_canonical, + canonical: e.canonical, + data: e.data, + })) + ); + } + for (const batch of batchIterate(values, INSERT_BATCH_SIZE)) { + await sql` + INSERT INTO pox5_events ${sql(batch)} + `; + } + } + private async updatePoxStateUnlockHeight(sql: PgSqlClient, data: DataStoreBlockUpdateData) { if (data.pox_v1_unlock_height !== undefined) { // update the pox_state.pox_v1_unlock_height singleton diff --git a/tests/api/cache/cache-control.test.ts b/tests/api/cache/cache-control.test.ts index 08db30830..585cb54c1 100644 --- a/tests/api/cache/cache-control.test.ts +++ b/tests/api/cache/cache-control.test.ts @@ -17,7 +17,7 @@ import { beforeEach, afterEach, describe, test } from 'node:test'; import assert from 'node:assert/strict'; import { assertMatchesObject } from '../test-helpers.ts'; import { STACKS_TESTNET } from '@stacks/network'; -import { SyntheticPoxEventName } from '../../../src/pox-helpers.ts'; +import { Pox4EventName } from '@stacks/codec'; describe('cache-control tests', () => { let db: PgWriteStore; @@ -123,6 +123,7 @@ describe('cache-control tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -296,6 +297,7 @@ describe('cache-control tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -1138,17 +1140,18 @@ describe('cache-control tests', () => { block_height: 2, canonical: true, stacker: stacker, - locked: 1000n, - balance: 5000n, - burnchain_unlock_height: 200n, + locked: '1000', + balance: '5000', + burnchain_unlock_height: '200', pox_addr: null, pox_addr_raw: null, - name: SyntheticPoxEventName.StackStx, + pox_version: 'pox4', + name: Pox4EventName.StackStx, data: { - lock_amount: 1000n, - lock_period: 1n, - start_burn_height: 100n, - unlock_burn_height: 200n, + lock_amount: '1000', + lock_period: '1', + start_burn_height: '100', + unlock_burn_height: '200', signer_key: '0x0011223344', end_cycle_id: null, start_cycle_id: null, diff --git a/tests/api/datastore/datastore.test.ts b/tests/api/datastore/datastore.test.ts index 24a8bc9f8..7ee43fe3f 100644 --- a/tests/api/datastore/datastore.test.ts +++ b/tests/api/datastore/datastore.test.ts @@ -392,6 +392,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -578,6 +579,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -820,6 +822,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], })), }); @@ -1024,6 +1027,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], })), }); @@ -1317,6 +1321,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: tx2, @@ -1331,6 +1336,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: tx3, @@ -1345,6 +1351,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2109,6 +2116,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2208,6 +2216,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2315,6 +2324,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2424,6 +2434,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2564,6 +2575,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2662,6 +2674,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2759,6 +2772,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -3023,6 +3037,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: { ...tx2, raw_tx: '0x' }, @@ -3037,6 +3052,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -3561,6 +3577,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -3625,6 +3642,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -4146,6 +4164,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -4213,6 +4232,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -4430,6 +4450,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -5144,6 +5165,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -5166,6 +5188,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: tx3, @@ -5180,6 +5203,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -5239,6 +5263,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -5387,6 +5412,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -5478,6 +5504,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -5623,6 +5650,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: tx2, @@ -5637,6 +5665,7 @@ describe('postgres datastore', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); diff --git a/tests/api/datastore/other.test.ts b/tests/api/datastore/other.test.ts index 5f659e404..454a9bc25 100644 --- a/tests/api/datastore/other.test.ts +++ b/tests/api/datastore/other.test.ts @@ -138,6 +138,7 @@ describe('other tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); diff --git a/tests/api/mempool/mempool.test.ts b/tests/api/mempool/mempool.test.ts index afdb0b7f6..80d169ba7 100644 --- a/tests/api/mempool/mempool.test.ts +++ b/tests/api/mempool/mempool.test.ts @@ -1465,6 +1465,7 @@ describe('mempool tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -1665,6 +1666,7 @@ describe('mempool tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); diff --git a/tests/api/microblocks/microblock.test.ts b/tests/api/microblocks/microblock.test.ts index 78f58ab38..350b722fd 100644 --- a/tests/api/microblocks/microblock.test.ts +++ b/tests/api/microblocks/microblock.test.ts @@ -387,6 +387,7 @@ describe('microblock tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -544,6 +545,7 @@ describe('microblock tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: mbTx2, @@ -558,6 +560,7 @@ describe('microblock tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); diff --git a/tests/api/principal/address.test.ts b/tests/api/principal/address.test.ts index e01cd7c70..12ad8e85c 100644 --- a/tests/api/principal/address.test.ts +++ b/tests/api/principal/address.test.ts @@ -242,6 +242,7 @@ describe('address tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], })), }); @@ -1535,6 +1536,7 @@ describe('address tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], } as DataStoreTxEventData; }); dataStoreTxs.push({ @@ -1550,6 +1552,7 @@ describe('address tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }); dataStoreTxs.push({ tx: { ...contractCall, raw_tx: '0x' }, @@ -1577,6 +1580,7 @@ describe('address tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }); await db.update({ block: block, @@ -2651,6 +2655,7 @@ describe('address tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2898,6 +2903,7 @@ describe('address tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }); } await db.updateMicroblocks(mbData); diff --git a/tests/api/smart-contracts/smart-contract.test.ts b/tests/api/smart-contracts/smart-contract.test.ts index 2fb2e1777..57b35a6b5 100644 --- a/tests/api/smart-contracts/smart-contract.test.ts +++ b/tests/api/smart-contracts/smart-contract.test.ts @@ -151,6 +151,7 @@ describe('smart contract tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: tx2, @@ -165,6 +166,7 @@ describe('smart contract tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -295,6 +297,7 @@ describe('smart contract tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -412,6 +415,7 @@ describe('smart contract tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -1605,6 +1609,7 @@ describe('smart contract tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: tx2, @@ -1619,6 +1624,7 @@ describe('smart contract tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); diff --git a/tests/api/test-builders.ts b/tests/api/test-builders.ts index 534f79087..ec0cd2fa6 100644 --- a/tests/api/test-builders.ts +++ b/tests/api/test-builders.ts @@ -280,6 +280,7 @@ function testTx(args?: TestTxArgs): DataStoreTxEventData { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }; if ( data.tx.type_id === DbTxTypeId.SmartContract || diff --git a/tests/api/transactions/search.test.ts b/tests/api/transactions/search.test.ts index 0ec0b4121..1b5461e7c 100644 --- a/tests/api/transactions/search.test.ts +++ b/tests/api/transactions/search.test.ts @@ -365,6 +365,7 @@ describe('search tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }; @@ -1317,6 +1318,7 @@ describe('search tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: stxTx2, @@ -1331,6 +1333,7 @@ describe('search tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: smartContractTx, @@ -1345,6 +1348,7 @@ describe('search tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }; diff --git a/tests/api/transactions/tx.test.ts b/tests/api/transactions/tx.test.ts index 14df6487a..a159d7055 100644 --- a/tests/api/transactions/tx.test.ts +++ b/tests/api/transactions/tx.test.ts @@ -277,6 +277,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: dbTx2, @@ -291,6 +292,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: dbTx3, @@ -305,6 +307,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -453,6 +456,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -678,6 +682,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -831,6 +836,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -1019,6 +1025,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -1406,6 +1413,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -1651,6 +1659,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -1862,6 +1871,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2020,6 +2030,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2815,6 +2826,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -2940,6 +2952,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -3596,6 +3609,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); @@ -3871,6 +3885,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, { tx: tx2, @@ -3885,6 +3900,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }; @@ -4192,6 +4208,7 @@ describe('tx tests', () => { pox2Events: [], pox3Events: [], pox4Events: [], + pox5Events: [], }, ], }); From ab7353d3c828ba2c8d762b718cfa032ffcfa3d42 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Fri, 22 May 2026 12:19:37 -0600 Subject: [PATCH 04/31] bond schemas --- .../v3/entities/pox5-bond-registrations.ts | 29 ++++++++++++++++++ src/api/schemas/v3/entities/pox5-bonds.ts | 30 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/api/schemas/v3/entities/pox5-bond-registrations.ts create mode 100644 src/api/schemas/v3/entities/pox5-bonds.ts diff --git a/src/api/schemas/v3/entities/pox5-bond-registrations.ts b/src/api/schemas/v3/entities/pox5-bond-registrations.ts new file mode 100644 index 000000000..40ae44ee1 --- /dev/null +++ b/src/api/schemas/v3/entities/pox5-bond-registrations.ts @@ -0,0 +1,29 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const Pox5BondRegistrationSchema = Type.Object({ + bond_index: Type.Integer(), + pox_address: Type.Optional( + Type.String({ + description: + 'Where they want to receive BTC rewards. If this is none, rewards are received as sBTC.', + }) + ), + signer_manager: Type.String(), + btc_lockup: Type.Union([ + Type.Object({ + type: Type.Literal('outputs'), + outputs: Type.Object({ + amount: Type.String(), + tx_id: Type.String(), + output_index: Type.Integer(), + }), + unlock_bytes: Type.String(), + }), + Type.Object({ + type: Type.Literal('sbtc'), + amount: Type.String(), + }), + ]), + signer_calldata: Type.Optional(Type.String()), +}); +export type Pox5BondRegistration = Static; diff --git a/src/api/schemas/v3/entities/pox5-bonds.ts b/src/api/schemas/v3/entities/pox5-bonds.ts new file mode 100644 index 000000000..1d0ccf126 --- /dev/null +++ b/src/api/schemas/v3/entities/pox5-bonds.ts @@ -0,0 +1,30 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const Pox5BondSummarySchema = Type.Object({ + tx_id: Type.String(), + index: Type.Integer(), + yield_rate: Type.Integer({ description: 'The target yield rate (APY) in basis points' }), + stx_value_ratio: Type.Integer({ + description: + 'This is a representation of the STXBTC price. The value represents "uSTX per 100 sats"', + }), + minimum_stx_ratio: Type.Integer({ + description: + 'The amount of STX that must be locked relative to BTC, in equal-valued terms (ie in USD terms). This value is represented in basis points.', + }), +}); +export type Pox5BondSummary = Static; + +export const Pox5BondAllowlistSchema = Type.Object({ + staker: Type.String(), + max_sats: Type.String(), +}); +export type Pox5BondAllowlist = Static; + +export const Pox5BondSchema = Type.Composite([ + Pox5BondSummarySchema, + Type.Object({ + early_unlock_signers: Type.String(), + }), +]); +export type Pox5Bond = Static; From dc1622d78d2c2ee4e9a4a3583269512d0d68b82f Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Mon, 25 May 2026 15:13:02 -0600 Subject: [PATCH 05/31] type and schema progress --- migrations/1779392293803_pox5-events.ts | 29 +++-- migrations/1779487960678_bonds.ts | 91 +++++++++++++++ .../1779487971367_bond-allowlist-entries.ts | 83 ++++++++++++++ .../1779487975550_bond-registrations.ts | 94 ++++++++++++++++ .../1779742831642_principal-bond-positions.ts | 80 ++++++++++++++ src/api/routes/v3/principals.ts | 15 +++ src/api/routes/v3/staking-bonds.ts | 104 ++++++++++++++++++ src/api/routes/v3/staking-principals.ts | 30 +++++ .../v3/entities/bond-allowlist-entries.ts | 7 ++ ...registrations.ts => bond-registrations.ts} | 0 src/api/schemas/v3/entities/bonds.ts | 87 +++++++++++++++ src/api/schemas/v3/entities/common.ts | 35 ++++++ src/api/schemas/v3/entities/pox5-bonds.ts | 30 ----- .../v3/entities/principal-bond-positions.ts | 35 ++++++ .../v3/entities/transaction-summaries.ts | 29 +---- .../principal-staking-balances-response.ts | 13 +++ 16 files changed, 696 insertions(+), 66 deletions(-) create mode 100644 migrations/1779487960678_bonds.ts create mode 100644 migrations/1779487971367_bond-allowlist-entries.ts create mode 100644 migrations/1779487975550_bond-registrations.ts create mode 100644 migrations/1779742831642_principal-bond-positions.ts create mode 100644 src/api/routes/v3/staking-bonds.ts create mode 100644 src/api/routes/v3/staking-principals.ts create mode 100644 src/api/schemas/v3/entities/bond-allowlist-entries.ts rename src/api/schemas/v3/entities/{pox5-bond-registrations.ts => bond-registrations.ts} (100%) create mode 100644 src/api/schemas/v3/entities/bonds.ts delete mode 100644 src/api/schemas/v3/entities/pox5-bonds.ts create mode 100644 src/api/schemas/v3/entities/principal-bond-positions.ts create mode 100644 src/api/schemas/v3/responses/principal-staking-balances-response.ts diff --git a/migrations/1779392293803_pox5-events.ts b/migrations/1779392293803_pox5-events.ts index ee45b5d10..f0fa36285 100644 --- a/migrations/1779392293803_pox5-events.ts +++ b/migrations/1779392293803_pox5-events.ts @@ -46,24 +46,33 @@ export const up = (pgm: MigrationBuilder) => { type: 'boolean', notNull: true, }, + name: { + type: 'text', + notNull: true, + }, data: { type: 'jsonb', notNull: true, - } + }, }); - pgm.createIndex('pox5_events', [ - { name: 'block_height', sort: 'DESC' }, - { name: 'microblock_sequence', sort: 'DESC' }, - { name: 'tx_index', sort: 'DESC' }, - { name: 'event_index', sort: 'DESC' }, - ]); + pgm.createIndex( + 'pox5_events', + [ + { name: 'block_height', sort: 'DESC' }, + { name: 'microblock_sequence', sort: 'DESC' }, + { name: 'tx_index', sort: 'DESC' }, + { name: 'event_index', sort: 'DESC' }, + ], + { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + } + ); pgm.createIndex('pox5_events', 'tx_id'); pgm.createIndex('pox5_events', ['index_block_hash', 'canonical']); pgm.createIndex('pox5_events', 'microblock_hash'); -} +}; export const down = (pgm: MigrationBuilder) => { pgm.dropTable('pox5_events'); -} - +}; diff --git a/migrations/1779487960678_bonds.ts b/migrations/1779487960678_bonds.ts new file mode 100644 index 000000000..3cbd2ca5c --- /dev/null +++ b/migrations/1779487960678_bonds.ts @@ -0,0 +1,91 @@ +import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export const up = (pgm: MigrationBuilder) => { + pgm.createTable('pox5_bonds', { + id: { + type: 'bigserial', + primaryKey: true, + }, + tx_id: { + type: 'bytea', + notNull: true, + }, + tx_index: { + type: 'smallint', + notNull: true, + }, + block_height: { + type: 'integer', + notNull: true, + }, + index_block_hash: { + type: 'bytea', + notNull: true, + }, + parent_index_block_hash: { + type: 'bytea', + notNull: true, + }, + microblock_hash: { + type: 'bytea', + notNull: true, + }, + microblock_sequence: { + type: 'integer', + notNull: true, + }, + microblock_canonical: { + type: 'boolean', + notNull: true, + }, + canonical: { + type: 'boolean', + notNull: true, + }, + bond_index: { + type: 'integer', + notNull: true, + }, + target_rate: { + type: 'integer', + notNull: true, + }, + stx_value_ratio: { + type: 'integer', + notNull: true, + }, + min_ustx_ratio: { + type: 'integer', + notNull: true, + }, + early_unlock_signers: { + type: 'text', + notNull: true, + }, + early_unlock_admin: { + type: 'text', + notNull: true, + } + }); + pgm.createIndex( + 'pox5_bonds', + [ + { name: 'block_height', sort: 'DESC' }, + { name: 'microblock_sequence', sort: 'DESC' }, + { name: 'tx_index', sort: 'DESC' }, + { name: 'event_index', sort: 'DESC' }, + ], + { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + } + ); + pgm.createIndex('pox5_bonds', 'bond_index', { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + }); +}; + +export const down = (pgm: MigrationBuilder) => { + pgm.dropTable('pox5_bonds'); +}; diff --git a/migrations/1779487971367_bond-allowlist-entries.ts b/migrations/1779487971367_bond-allowlist-entries.ts new file mode 100644 index 000000000..9b53471b5 --- /dev/null +++ b/migrations/1779487971367_bond-allowlist-entries.ts @@ -0,0 +1,83 @@ +import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export const up = (pgm: MigrationBuilder) => { + pgm.createTable('pox5_bond_allowlist_entries', { + id: { + type: 'bigserial', + primaryKey: true, + }, + tx_id: { + type: 'bytea', + notNull: true, + }, + tx_index: { + type: 'smallint', + notNull: true, + }, + block_height: { + type: 'integer', + notNull: true, + }, + index_block_hash: { + type: 'bytea', + notNull: true, + }, + parent_index_block_hash: { + type: 'bytea', + notNull: true, + }, + microblock_hash: { + type: 'bytea', + notNull: true, + }, + microblock_sequence: { + type: 'integer', + notNull: true, + }, + microblock_canonical: { + type: 'boolean', + notNull: true, + }, + canonical: { + type: 'boolean', + notNull: true, + }, + bond_index: { + type: 'integer', + notNull: true, + }, + staker: { + type: 'string', + notNull: true, + }, + max_sats: { + type: 'string', + notNull: true, + }, + }); + + pgm.createIndex( + 'pox5_bond_allowlist_entries', + [ + { name: 'block_height', sort: 'DESC' }, + { name: 'microblock_sequence', sort: 'DESC' }, + { name: 'tx_index', sort: 'DESC' }, + { name: 'event_index', sort: 'DESC' }, + ], + { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + } + ); + pgm.createIndex('pox5_bond_allowlist_entries', 'bond_index', { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + }); + pgm.createIndex('pox5_bond_allowlist_entries', 'staker', { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + }); +}; + +export const down = (pgm: MigrationBuilder) => { + pgm.dropTable('pox5_bond_allowlist_entries'); +}; diff --git a/migrations/1779487975550_bond-registrations.ts b/migrations/1779487975550_bond-registrations.ts new file mode 100644 index 000000000..798b6cf5e --- /dev/null +++ b/migrations/1779487975550_bond-registrations.ts @@ -0,0 +1,94 @@ +import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export const up = (pgm: MigrationBuilder) => { + pgm.createTable('pox5_bond_registrations', { + id: { + type: 'bigserial', + primaryKey: true, + }, + tx_id: { + type: 'bytea', + notNull: true, + }, + tx_index: { + type: 'smallint', + notNull: true, + }, + block_height: { + type: 'integer', + notNull: true, + }, + index_block_hash: { + type: 'bytea', + notNull: true, + }, + parent_index_block_hash: { + type: 'bytea', + notNull: true, + }, + microblock_hash: { + type: 'bytea', + notNull: true, + }, + microblock_sequence: { + type: 'integer', + notNull: true, + }, + microblock_canonical: { + type: 'boolean', + notNull: true, + }, + canonical: { + type: 'boolean', + notNull: true, + }, + bond_index: { + type: 'integer', + notNull: true, + }, + pox_address: { + type: 'string', + notNull: true, + }, + signer_manager: { + type: 'string', + notNull: true, + }, + btc_lockup: { + type: 'jsonb', + notNull: true, + }, + signer_calldata: { + type: 'string', + notNull: true, + }, + }); + + pgm.createIndex('pox5_bond_registrations', 'bond_index', { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + }); + pgm.createIndex('pox5_bond_registrations', 'pox_address', { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + }); + pgm.createIndex('pox5_bond_registrations', 'signer_manager', { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + }); + pgm.createIndex( + 'pox5_bond_registrations', + [ + { name: 'block_height', sort: 'DESC' }, + { name: 'microblock_sequence', sort: 'DESC' }, + { name: 'tx_index', sort: 'DESC' }, + { name: 'event_index', sort: 'DESC' }, + ], + { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + } + ); +}; + +export const down = (pgm: MigrationBuilder) => { + pgm.dropTable('pox5_bond_registrations'); +}; diff --git a/migrations/1779742831642_principal-bond-positions.ts b/migrations/1779742831642_principal-bond-positions.ts new file mode 100644 index 000000000..b07b3dd04 --- /dev/null +++ b/migrations/1779742831642_principal-bond-positions.ts @@ -0,0 +1,80 @@ +import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export function up(pgm: MigrationBuilder): void { + pgm.createTable('principal_bond_positions', { + id: { + type: 'bigserial', + primaryKey: true, + }, + principal: { + type: 'string', + notNull: true, + }, + bond_index: { + type: 'integer', + notNull: true, + }, + tx_id: { + type: 'bytea', + notNull: true, + }, + tx_index: { + type: 'smallint', + notNull: true, + }, + block_height: { + type: 'integer', + notNull: true, + }, + index_block_hash: { + type: 'bytea', + notNull: true, + }, + parent_index_block_hash: { + type: 'bytea', + notNull: true, + }, + microblock_hash: { + type: 'bytea', + notNull: true, + }, + microblock_sequence: { + type: 'integer', + notNull: true, + }, + microblock_canonical: { + type: 'boolean', + notNull: true, + }, + canonical: { + type: 'boolean', + notNull: true, + }, + status: { + type: 'smallint', + notNull: true, + }, + active: { + type: 'boolean', + notNull: true, + }, + btc_locked: { + type: 'numeric', + notNull: true, + }, + stx_locked: { + type: 'numeric', + notNull: true, + }, + btc_paid_out: { + type: 'numeric', + notNull: true, + }, + }); +} + +export function down(pgm: MigrationBuilder): void { + pgm.dropTable('principal_bond_positions'); +} diff --git a/src/api/routes/v3/principals.ts b/src/api/routes/v3/principals.ts index 5e725b029..8cc412533 100644 --- a/src/api/routes/v3/principals.ts +++ b/src/api/routes/v3/principals.ts @@ -56,5 +56,20 @@ export const PrincipalsRoutes: FastifyPluginAsync< } ); + fastify.get( + '/principals/:principal/balances/staking', + { + schema: { + operationId: 'get_principal_staking_balances', + summary: 'Get principal staking balances', + description: 'Get principal staking balances', + tags: ['Staking'], + }, + }, + async (_req, reply) => { + await reply.send(); + } + ); + await Promise.resolve(); }; diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts new file mode 100644 index 000000000..7655bed7a --- /dev/null +++ b/src/api/routes/v3/staking-bonds.ts @@ -0,0 +1,104 @@ +import { FastifyPluginAsync } from 'fastify'; +import { Server } from 'node:http'; +import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; + +export const StakingBondsRoutes: FastifyPluginAsync< + Record, + Server, + TypeBoxTypeProvider +> = async fastify => { + fastify.get( + '/staking/bonds', + { + schema: { + operationId: 'get_bonds', + summary: 'Get bonds', + description: 'Get bonds', + tags: ['Staking'], + }, + }, + async (_req, reply) => { + await reply.send(); + } + ); + + fastify.get( + '/staking/bonds/:bond_index', + { + schema: { + operationId: 'get_bond', + summary: 'Get bond', + description: 'Get bond', + tags: ['Staking'], + params: Type.Object({ + bond_index: Type.Integer({ description: 'Bond index' }), + }), + }, + }, + async (_req, reply) => { + await reply.send(); + } + ); + + fastify.get( + '/staking/bonds/:bond_index/allowlist', + { + schema: { + operationId: 'get_bond_allowlist_entries', + summary: 'Get bond allowlist entries', + description: 'Get bond allowlist entries', + tags: ['Staking'], + }, + }, + async (_req, reply) => { + await reply.send(); + } + ); + + fastify.get( + '/staking/bonds/:bond_index/allowlist/:principal', + { + schema: { + operationId: 'get_bond_allowlist_entry', + summary: 'Get bond allowlist entry', + description: 'Get bond allowlist entry', + tags: ['Staking'], + }, + }, + async (_req, reply) => { + await reply.send(); + } + ); + + fastify.get( + '/staking/bonds/:bond_index/registrations', + { + schema: { + operationId: 'get_pox5_bond_allowlist_entries', + summary: 'Get PoX-5 bond allowlist entries', + description: 'Get PoX-5 bond allowlist entries', + tags: ['PoX-5'], + }, + }, + async (_req, reply) => { + await reply.send(); + } + ); + + fastify.get( + '/staking/bonds/:bond_index/registrations/:principal', + { + schema: { + operationId: 'get_bond_registration', + summary: 'Get bond registration', + description: 'Get bond registration', + tags: ['Staking'], + }, + }, + async (_req, reply) => { + await reply.send(); + } + ); + + await Promise.resolve(); +}; diff --git a/src/api/routes/v3/staking-principals.ts b/src/api/routes/v3/staking-principals.ts new file mode 100644 index 000000000..791707400 --- /dev/null +++ b/src/api/routes/v3/staking-principals.ts @@ -0,0 +1,30 @@ +import { FastifyPluginAsync } from 'fastify'; +import { Server } from 'node:http'; +import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; +import { PrincipalSchema } from '../../schemas/v3/entities/common.js'; + +export const StakingPrincipalsRoutes: FastifyPluginAsync< + Record, + Server, + TypeBoxTypeProvider +> = async fastify => { + fastify.get( + '/staking/principals/:principal/positions', + { + schema: { + operationId: 'get_principal_staking_positions', + summary: 'Get principal staking positions', + description: 'Get principal staking positions', + tags: ['Staking'], + params: Type.Object({ + principal: PrincipalSchema, + }), + }, + }, + async (_req, reply) => { + await reply.send(); + } + ); + + await Promise.resolve(); +}; diff --git a/src/api/schemas/v3/entities/bond-allowlist-entries.ts b/src/api/schemas/v3/entities/bond-allowlist-entries.ts new file mode 100644 index 000000000..5f62d0cc2 --- /dev/null +++ b/src/api/schemas/v3/entities/bond-allowlist-entries.ts @@ -0,0 +1,7 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const BondAllowlistSchema = Type.Object({ + staker: Type.String(), + max_sats: Type.String(), +}); +export type BondAllowlist = Static; diff --git a/src/api/schemas/v3/entities/pox5-bond-registrations.ts b/src/api/schemas/v3/entities/bond-registrations.ts similarity index 100% rename from src/api/schemas/v3/entities/pox5-bond-registrations.ts rename to src/api/schemas/v3/entities/bond-registrations.ts diff --git a/src/api/schemas/v3/entities/bonds.ts b/src/api/schemas/v3/entities/bonds.ts new file mode 100644 index 000000000..972030b8d --- /dev/null +++ b/src/api/schemas/v3/entities/bonds.ts @@ -0,0 +1,87 @@ +import { Static, Type } from '@sinclair/typebox'; +import { BitcoinBlockPositionSchema, BlockPositionSchema, BondIndexSchema } from './common.js'; + +export const BondStatusSchema = Type.Union([ + Type.Literal('upcoming'), + Type.Literal('active'), + Type.Literal('unlocked'), +]); +export type BondStatus = Static; + +export const BondBalancesSchema = Type.Object({ + locked: Type.Object({ + btc: Type.String({ + description: 'The total amount of BTC that is locked up for this bond', + }), + stx: Type.String({ + description: 'The total amount of STX that is locked up for this bond', + }), + }), + paid_out: Type.Object({ + btc: Type.String({ + description: 'The total amount of BTC that has been paid out for this bond', + }), + }), +}); +export type BondBalances = Static; + +export const BondSummarySchema = Type.Object({ + index: BondIndexSchema, + pox_version: Type.Literal('pox5'), + status: BondStatusSchema, + parameters: Type.Object({ + target_rate_bps: Type.Integer({ description: 'The target yield rate (APY) in basis points' }), + stx_value_ratio: Type.Integer({ + description: + 'This is a representation of the STXBTC price. The value represents "uSTX per 100 sats"', + }), + minimum_stx_ratio: Type.Integer({ + description: + 'The amount of STX that must be locked relative to BTC, in equal-valued terms (ie in USD terms). This value is represented in basis points.', + }), + btc_capacity: Type.String({ + description: 'The total capacity of BTC that can be locked up for this bond', + }), + }), + registrations: Type.Object({ + allowed_count: Type.Integer({ + description: 'The number of entries in the allowlist for this bond', + }), + registered_count: Type.Integer({ + description: 'The number of registrations for this bond', + }), + }), + schedule: Type.Object({ + activation: Type.Object({ + bitcoin_height: Type.Integer({ + description: 'The height at which the bond was activated', + }), + pox_cycle: Type.Integer({ + description: 'The POX cycle at which the bond was activated', + }), + }), + unlock: Type.Object({ + bitcoin_height: Type.Integer({ + description: 'The height at which the bond can be unlocked', + }), + pox_cycle: Type.Integer({ + description: 'The POX cycle at which the bond can be unlocked', + }), + }), + }), + balances: BondBalancesSchema, +}); +export type BondSummary = Static; + +export const BondSchema = Type.Composite([ + BondSummarySchema, + Type.Object({ + transaction: Type.Object({ + tx_id: Type.String({ description: 'The transaction ID that created the bond' }), + block: BlockPositionSchema, + bitcoin_block: BitcoinBlockPositionSchema, + }), + early_unlock_signers: Type.String(), + }), +]); +export type Bond = Static; diff --git a/src/api/schemas/v3/entities/common.ts b/src/api/schemas/v3/entities/common.ts index 3690e6bf3..8547725d2 100644 --- a/src/api/schemas/v3/entities/common.ts +++ b/src/api/schemas/v3/entities/common.ts @@ -51,6 +51,11 @@ export const BlockHeightOrHashSchema = Type.Union([ ]); export type BlockHeightOrHash = Static; +export const BondIndexSchema = Type.Integer({ + description: 'The index of the bond in the PoX-5 bond list', +}); +export type BondIndex = Static; + export const DecodedClarityValueSchema = Type.Object({ hex: Type.String(), repr: Type.String(), @@ -75,3 +80,33 @@ export const ExecutionCostSchema = Type.Object({ }), }); export type ExecutionCost = Static; + +export const BlockPositionSchema = Type.Object({ + height: Type.Integer({ + description: 'Height of the block this transactions was associated with', + }), + hash: Type.String({ + description: 'Hash of the blocked this transactions was associated with', + }), + index_hash: Type.String({ + description: 'Hash of the index block this transactions was associated with', + }), + time: Type.Number({ + description: 'Unix timestamp (in seconds) indicating when this block was mined.', + }), + tx_index: Type.Integer({ + description: + 'Index of the transaction, indicating the order. Starts at `0` and increases with each transaction', + }), +}); +export type BlockPosition = Static; + +export const BitcoinBlockPositionSchema = Type.Object({ + height: Type.Integer({ + description: 'Height of the anchor burn block.', + }), + time: Type.Number({ + description: 'Unix timestamp (in seconds) indicating when this block was mined.', + }), +}); +export type BitcoinBlockPosition = Static; diff --git a/src/api/schemas/v3/entities/pox5-bonds.ts b/src/api/schemas/v3/entities/pox5-bonds.ts deleted file mode 100644 index 1d0ccf126..000000000 --- a/src/api/schemas/v3/entities/pox5-bonds.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Static, Type } from '@sinclair/typebox'; - -export const Pox5BondSummarySchema = Type.Object({ - tx_id: Type.String(), - index: Type.Integer(), - yield_rate: Type.Integer({ description: 'The target yield rate (APY) in basis points' }), - stx_value_ratio: Type.Integer({ - description: - 'This is a representation of the STXBTC price. The value represents "uSTX per 100 sats"', - }), - minimum_stx_ratio: Type.Integer({ - description: - 'The amount of STX that must be locked relative to BTC, in equal-valued terms (ie in USD terms). This value is represented in basis points.', - }), -}); -export type Pox5BondSummary = Static; - -export const Pox5BondAllowlistSchema = Type.Object({ - staker: Type.String(), - max_sats: Type.String(), -}); -export type Pox5BondAllowlist = Static; - -export const Pox5BondSchema = Type.Composite([ - Pox5BondSummarySchema, - Type.Object({ - early_unlock_signers: Type.String(), - }), -]); -export type Pox5Bond = Static; diff --git a/src/api/schemas/v3/entities/principal-bond-positions.ts b/src/api/schemas/v3/entities/principal-bond-positions.ts new file mode 100644 index 000000000..efa37ab81 --- /dev/null +++ b/src/api/schemas/v3/entities/principal-bond-positions.ts @@ -0,0 +1,35 @@ +import { Static, Type } from '@sinclair/typebox'; +import { BondBalancesSchema } from './bonds.js'; +import { BondIndexSchema, TransactionIdSchema } from './common.js'; + +export const PrincipalBondPositionStatusSchema = Type.Union([ + Type.Literal('enrolled'), + Type.Literal('running'), + Type.Literal('early_exit'), + Type.Literal('unlocked'), +]); +export type PrincipalBondPositionStatus = Static; + +export const PrincipalBondPositionSchema = Type.Object({ + bond_index: BondIndexSchema, + status: PrincipalBondPositionStatusSchema, + active: Type.Boolean({ + description: 'Whether the position is active', + }), + balances: BondBalancesSchema, + enrollment: Type.Object({ + tx_id: TransactionIdSchema, + pox_address: Type.String({ + description: 'The POX address of the principal', + }), + btc_lockup: Type.Object({ + amount: Type.String({ + description: 'The amount of BTC that is locked up for this principal', + }), + }), + }), + amount: Type.String({ + description: 'The amount of STX that is locked up for this principal', + }), +}); +export type PrincipalBondPosition = Static; diff --git a/src/api/schemas/v3/entities/transaction-summaries.ts b/src/api/schemas/v3/entities/transaction-summaries.ts index 23a2e7460..dde4d1dd0 100644 --- a/src/api/schemas/v3/entities/transaction-summaries.ts +++ b/src/api/schemas/v3/entities/transaction-summaries.ts @@ -1,5 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { Nullable } from '../../v1/util.js'; +import { BitcoinBlockPositionSchema, BlockPositionSchema } from './common.js'; export const TransactionSenderSchema = Type.Object({ address: Type.String({ @@ -47,32 +48,8 @@ export const BaseTransactionSummarySchema = Type.Object({ fee_rate: Type.String({ description: 'Transaction fee as Integer string (64-bit unsigned integer).', }), - block: Type.Object({ - height: Type.Integer({ - description: 'Height of the block this transactions was associated with', - }), - hash: Type.String({ - description: 'Hash of the blocked this transactions was associated with', - }), - index_hash: Type.String({ - description: 'Hash of the index block this transactions was associated with', - }), - time: Type.Number({ - description: 'Unix timestamp (in seconds) indicating when this block was mined.', - }), - tx_index: Type.Integer({ - description: - 'Index of the transaction, indicating the order. Starts at `0` and increases with each transaction', - }), - }), - bitcoin_block: Type.Object({ - height: Type.Integer({ - description: 'Height of the anchor burn block.', - }), - time: Type.Number({ - description: 'Unix timestamp (in seconds) indicating when this block was mined.', - }), - }), + block: BlockPositionSchema, + bitcoin_block: BitcoinBlockPositionSchema, status: TransactionStatusSchema, }); export type BaseTransactionSummary = Static; diff --git a/src/api/schemas/v3/responses/principal-staking-balances-response.ts b/src/api/schemas/v3/responses/principal-staking-balances-response.ts new file mode 100644 index 000000000..ce1deb10d --- /dev/null +++ b/src/api/schemas/v3/responses/principal-staking-balances-response.ts @@ -0,0 +1,13 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const PrincipalStakingBalancesResponseSchema = Type.Object({ + bonds: Type.String({ + description: 'The total amount of STX that is locked up for this principal', + }), + stx: Type.String({ + description: 'The total amount of STX that is locked up for this principal', + }), +}); +export type PrincipalStakingBalancesResponse = Static< + typeof PrincipalStakingBalancesResponseSchema +>; From fed30879cba5b052bf279a3dd55575c361ad6a84 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 26 May 2026 08:25:44 -0600 Subject: [PATCH 06/31] rewards start --- .../1779487975550_bond-registrations.ts | 32 ++++++++++++++----- ...1779745340752_bond-reward-distributions.ts | 7 ++++ 2 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 migrations/1779745340752_bond-reward-distributions.ts diff --git a/migrations/1779487975550_bond-registrations.ts b/migrations/1779487975550_bond-registrations.ts index 798b6cf5e..51b784a82 100644 --- a/migrations/1779487975550_bond-registrations.ts +++ b/migrations/1779487975550_bond-registrations.ts @@ -48,20 +48,36 @@ export const up = (pgm: MigrationBuilder) => { type: 'integer', notNull: true, }, - pox_address: { - type: 'string', + signer: { + type: 'text', notNull: true, }, - signer_manager: { - type: 'string', + staker: { + type: 'text', notNull: true, }, - btc_lockup: { - type: 'jsonb', + amount_ustx: { + type: 'text', notNull: true, }, - signer_calldata: { - type: 'string', + sats_total: { + type: 'text', + notNull: true, + }, + first_reward_cycle: { + type: 'integer', + notNull: true, + }, + unlock_burn_height: { + type: 'integer', + notNull: true, + }, + unlock_cycle: { + type: 'integer', + notNull: true, + }, + is_l1_lock: { + type: 'boolean', notNull: true, }, }); diff --git a/migrations/1779745340752_bond-reward-distributions.ts b/migrations/1779745340752_bond-reward-distributions.ts new file mode 100644 index 000000000..6ba1556f7 --- /dev/null +++ b/migrations/1779745340752_bond-reward-distributions.ts @@ -0,0 +1,7 @@ +import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export async function up(pgm: MigrationBuilder): Promise {} + +export async function down(pgm: MigrationBuilder): Promise {} From c90cd50cef64b9d059872e3f8b368ba4e74d7bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20C=C3=A1rdenas?= <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 26 May 2026 14:08:35 -0600 Subject: [PATCH 07/31] fix: perserve block boundaries for v3 transaction cursors (#2565) * parse tx cursor * tests --- src/api/schemas/v3/cursors.ts | 4 +- src/datastore/v3/helpers.ts | 42 ++++++++++ src/datastore/v3/pg-store-v3.ts | 125 ++++++++++++++++-------------- tests/api/v3/blocks.test.ts | 75 ++++++++++++++++++ tests/api/v3/principals.test.ts | 115 +++++++++++++++++++++++++++ tests/api/v3/transactions.test.ts | 75 ++++++++++++++++++ 6 files changed, 378 insertions(+), 58 deletions(-) create mode 100644 src/datastore/v3/helpers.ts diff --git a/src/api/schemas/v3/cursors.ts b/src/api/schemas/v3/cursors.ts index 7514e7e91..d8271aad4 100644 --- a/src/api/schemas/v3/cursors.ts +++ b/src/api/schemas/v3/cursors.ts @@ -1,4 +1,4 @@ -import { ObjectOptions, TSchema, Type } from '@sinclair/typebox'; +import { ObjectOptions, Static, TSchema, Type } from '@sinclair/typebox'; import { pagingQueryLimits, ResourceType } from '../../pagination.js'; import { Nullable } from '../v1/util.js'; @@ -58,8 +58,10 @@ export const TransactionCursorSchema = Type.String({ 'Cursor for paginating transactions. Format: block_height:microblock_sequence:tx_index', pattern: '^[0-9]+:[0-9]+:[0-9]+$', }); +export type TransactionCursor = Static; export const MempoolTransactionCursorSchema = Type.String({ pattern: '^\\d+:(0x)?[a-fA-F0-9]{64}$', description: 'Cursor for paginating mempool transactions. Format: receipt_time:tx_id', }); +export type MempoolTransactionCursor = Static; diff --git a/src/datastore/v3/helpers.ts b/src/datastore/v3/helpers.ts new file mode 100644 index 000000000..faff452d0 --- /dev/null +++ b/src/datastore/v3/helpers.ts @@ -0,0 +1,42 @@ +import { TransactionCursor } from '../../api/schemas/v3/cursors.js'; +import { I32_MAX } from '../../helpers.js'; + +const MAX_TX_INDEX = 0x7fff; + +export type TransactionCursorRow = { + block_height: number; + microblock_sequence: number; + tx_index: number; +}; + +const parseTransactionCursor = (cursor: TransactionCursor): TransactionCursorRow => { + const [blockHeightStr, microblockSequenceStr, txIndexStr] = cursor.split(':'); + return { + block_height: parseInt(blockHeightStr, 10), + microblock_sequence: parseInt(microblockSequenceStr, 10), + tx_index: parseInt(txIndexStr, 10), + }; +}; + +/** + * Resolves a transaction cursor to a transaction cursor row. + * @param cursor - The transaction cursor. + * @param exactCursorExists - A function that checks if a cursor exists. + * @returns The transaction cursor row. + */ +export const resolveTransactionCursor = async ( + cursor: TransactionCursor, + exactCursorExists: (cursor: TransactionCursorRow) => Promise +): Promise => { + const parsed = parseTransactionCursor(cursor); + if (parsed.microblock_sequence !== 0 || parsed.tx_index !== 0) { + return parsed; + } + if (await exactCursorExists(parsed)) { + return parsed; + } + return { ...parsed, microblock_sequence: I32_MAX, tx_index: MAX_TX_INDEX }; +}; + +export const encodeTransactionCursor = (tx: TransactionCursorRow): TransactionCursor => + `${tx.block_height}:${tx.microblock_sequence}:${tx.tx_index}`; diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index 5a503cdf5..c6b52a405 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -19,6 +19,8 @@ import { normalizeHashString } from '../../helpers.js'; import { BlockIdParam } from '../../api/routes/v2/schemas.js'; import { InvalidRequestError, InvalidRequestErrorType } from '../../errors.js'; import { TransactionIncludeField } from '../../api/schemas/v3/entities/transactions.js'; +import type { TransactionCursor } from '../../api/schemas/v3/cursors.js'; +import { encodeTransactionCursor, resolveTransactionCursor } from './helpers.js'; export class PgStoreV3 extends BasePgStoreModule { /** @@ -28,19 +30,27 @@ export class PgStoreV3 extends BasePgStoreModule { */ async getTransactionSummaries(args: { limit: number; - cursor?: string; + cursor?: TransactionCursor; }): Promise> { return await this.sqlTransaction(async sql => { let cursorFilter = sql``; if (args.cursor) { - const parts = args.cursor.split(':'); - const [blockHeightStr, microblockSequenceStr, txIndexStr] = parts; - const blockHeight = parseInt(blockHeightStr, 10); - const microblockSequence = parseInt(microblockSequenceStr, 10); - const txIndex = parseInt(txIndexStr, 10); + const cursor = await resolveTransactionCursor(args.cursor, async cursor => { + const exactCursorQuery = await sql<{ exists: boolean }[]>` + SELECT EXISTS ( + SELECT 1 + FROM txs + WHERE canonical = true + AND microblock_canonical = true + AND (block_height, microblock_sequence, tx_index) + = (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) + ) AS exists + `; + return exactCursorQuery[0]?.exists ?? false; + }); cursorFilter = sql` AND (block_height, microblock_sequence, tx_index) - <= (${blockHeight}, ${microblockSequence}, ${txIndex}) + <= (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) `; } const resultQuery = await sql< @@ -62,15 +72,10 @@ export class PgStoreV3 extends BasePgStoreModule { const total = resultQuery.count > 0 ? resultQuery[0].total : 0; const nextResult = resultQuery[resultQuery.length - 1]; - const nextCursor = - hasNextPage && nextResult - ? `${nextResult.block_height}:${nextResult.microblock_sequence}:${nextResult.tx_index}` - : null; + const nextCursor = hasNextPage && nextResult ? encodeTransactionCursor(nextResult) : null; const firstResult = results[0]; - const currentCursor = firstResult - ? `${firstResult.block_height}:${firstResult.microblock_sequence}:${firstResult.tx_index}` - : null; + const currentCursor = firstResult ? encodeTransactionCursor(firstResult) : null; let prevCursor: string | null = null; if (firstResult) { @@ -88,12 +93,11 @@ export class PgStoreV3 extends BasePgStoreModule { ${firstResult.tx_index} ) ORDER BY block_height ASC, microblock_sequence ASC, tx_index ASC - OFFSET ${args.limit - 1} - LIMIT 1 + LIMIT ${args.limit} `; if (prevPageQuery.length > 0) { - const prevPage = prevPageQuery[0]; - prevCursor = `${prevPage.block_height}:${prevPage.microblock_sequence}:${prevPage.tx_index}`; + const prevPage = prevPageQuery[prevPageQuery.length - 1]; + prevCursor = encodeTransactionCursor(prevPage); } } @@ -117,19 +121,28 @@ export class PgStoreV3 extends BasePgStoreModule { async getPrincipalTransactionSummaries(args: { principal: Principal; limit: number; - cursor?: string; + cursor?: TransactionCursor; }): Promise> { return await this.sqlTransaction(async sql => { let cursorFilter = sql``; if (args.cursor) { - const parts = args.cursor.split(':'); - const [blockHeightStr, microblockSequenceStr, txIndexStr] = parts; - const blockHeight = parseInt(blockHeightStr, 10); - const microblockSequence = parseInt(microblockSequenceStr, 10); - const txIndex = parseInt(txIndexStr, 10); + const cursor = await resolveTransactionCursor(args.cursor, async cursor => { + const exactCursorQuery = await sql<{ exists: boolean }[]>` + SELECT EXISTS ( + SELECT 1 + FROM principal_txs + WHERE canonical = true + AND microblock_canonical = true + AND principal = ${args.principal} + AND (block_height, microblock_sequence, tx_index) + = (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) + ) AS exists + `; + return exactCursorQuery[0]?.exists ?? false; + }); cursorFilter = sql` AND (block_height, microblock_sequence, tx_index) - <= (${blockHeight}, ${microblockSequence}, ${txIndex}) + <= (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) `; } const resultQuery = await sql< @@ -182,15 +195,10 @@ export class PgStoreV3 extends BasePgStoreModule { const total = resultQuery.count > 0 ? resultQuery[0].total : 0; const nextResult = resultQuery[resultQuery.length - 1]; - const nextCursor = - hasNextPage && nextResult - ? `${nextResult.block_height}:${nextResult.microblock_sequence}:${nextResult.tx_index}` - : null; + const nextCursor = hasNextPage && nextResult ? encodeTransactionCursor(nextResult) : null; const firstResult = results[0]; - const currentCursor = firstResult - ? `${firstResult.block_height}:${firstResult.microblock_sequence}:${firstResult.tx_index}` - : null; + const currentCursor = firstResult ? encodeTransactionCursor(firstResult) : null; let prevCursor: string | null = null; if (firstResult) { @@ -209,12 +217,11 @@ export class PgStoreV3 extends BasePgStoreModule { ${firstResult.tx_index} ) ORDER BY block_height ASC, microblock_sequence ASC, tx_index ASC - OFFSET ${args.limit - 1} - LIMIT 1 + LIMIT ${args.limit} `; if (prevPageQuery.length > 0) { - const prevPage = prevPageQuery[0]; - prevCursor = `${prevPage.block_height}:${prevPage.microblock_sequence}:${prevPage.tx_index}`; + const prevPage = prevPageQuery[prevPageQuery.length - 1]; + prevCursor = encodeTransactionCursor(prevPage); } } @@ -278,11 +285,12 @@ export class PgStoreV3 extends BasePgStoreModule { WHERE pruned = false AND (receipt_time, tx_id) > (${firstResult.receipt_time}, ${firstResult.tx_id}) ORDER BY receipt_time ASC, tx_id ASC - OFFSET ${args.limit - 1} - LIMIT 1 + LIMIT ${args.limit} `; prevCursor = - prevPageQuery.length > 0 ? encodeMempoolTxSummaryCursor(prevPageQuery[0]) : null; + prevPageQuery.length > 0 + ? encodeMempoolTxSummaryCursor(prevPageQuery[prevPageQuery.length - 1]) + : null; } return { @@ -305,7 +313,7 @@ export class PgStoreV3 extends BasePgStoreModule { async getBlockTransactionSummaries(args: { block: BlockIdParam; limit: number; - cursor?: string; + cursor?: TransactionCursor; }): Promise> { return await this.sqlTransaction(async sql => { const blockFilter = @@ -332,14 +340,23 @@ export class PgStoreV3 extends BasePgStoreModule { let cursorFilter = sql``; if (args.cursor) { - const parts = args.cursor.split(':'); - const [blockHeightStr, microblockSequenceStr, txIndexStr] = parts; - const blockHeight = parseInt(blockHeightStr, 10); - const microblockSequence = parseInt(microblockSequenceStr, 10); - const txIndex = parseInt(txIndexStr, 10); + const cursor = await resolveTransactionCursor(args.cursor, async cursor => { + const exactCursorQuery = await sql<{ exists: boolean }[]>` + SELECT EXISTS ( + SELECT 1 + FROM txs + WHERE canonical = true + AND microblock_canonical = true + AND index_block_hash = ${index_block_hash} + AND (block_height, microblock_sequence, tx_index) + = (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) + ) AS exists + `; + return exactCursorQuery[0]?.exists ?? false; + }); cursorFilter = sql` AND (block_height, microblock_sequence, tx_index) - <= (${blockHeight}, ${microblockSequence}, ${txIndex}) + <= (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) `; } @@ -358,15 +375,10 @@ export class PgStoreV3 extends BasePgStoreModule { const results = hasNextPage ? resultQuery.slice(0, args.limit) : resultQuery; const nextResult = resultQuery[resultQuery.length - 1]; - const nextCursor = - hasNextPage && nextResult - ? `${nextResult.block_height}:${nextResult.microblock_sequence}:${nextResult.tx_index}` - : null; + const nextCursor = hasNextPage && nextResult ? encodeTransactionCursor(nextResult) : null; const firstResult = results[0]; - const currentCursor = firstResult - ? `${firstResult.block_height}:${firstResult.microblock_sequence}:${firstResult.tx_index}` - : null; + const currentCursor = firstResult ? encodeTransactionCursor(firstResult) : null; let prevCursor: string | null = null; if (firstResult) { @@ -385,12 +397,11 @@ export class PgStoreV3 extends BasePgStoreModule { ${firstResult.tx_index} ) ORDER BY block_height ASC, microblock_sequence ASC, tx_index ASC - OFFSET ${args.limit - 1} - LIMIT 1 + LIMIT ${args.limit} `; if (prevPageQuery.length > 0) { - const prevPage = prevPageQuery[0]; - prevCursor = `${prevPage.block_height}:${prevPage.microblock_sequence}:${prevPage.tx_index}`; + const prevPage = prevPageQuery[prevPageQuery.length - 1]; + prevCursor = encodeTransactionCursor(prevPage); } } diff --git a/tests/api/v3/blocks.test.ts b/tests/api/v3/blocks.test.ts index 620af3e50..880ae2547 100644 --- a/tests/api/v3/blocks.test.ts +++ b/tests/api/v3/blocks.test.ts @@ -7,6 +7,7 @@ import * as assert from 'node:assert/strict'; import { TestBlockBuilder } from '../test-builders.ts'; import { DbTxStatus, DbTxTypeId } from '../../../src/datastore/common.ts'; import { hex } from '../test-helpers.ts'; +import { I32_MAX } from '../../../src/helpers.ts'; const SENDER = 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27'; const RECIPIENT = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6'; @@ -403,6 +404,80 @@ describe('blocks', () => { assert.equal(body.cursor.current, '2:0:0'); }); + test('should allow block-boundary cursors for anchored transactions', async () => { + await db.update( + new TestBlockBuilder({ + block_height: 1, + index_block_hash: hex(1), + parent_index_block_hash: hex(0), + parent_block_hash: hex(0), + }) + .addTx({ + tx_id: hex(0x1001), + microblock_sequence: I32_MAX, + }) + .build() + ); + + const response = await api.fastifyApp.inject({ + method: 'GET', + url: '/extended/v3/blocks/1/transactions', + query: { + limit: '1', + cursor: '1:0:0', + }, + }); + assert.equal(response.statusCode, 200); + const body = JSON.parse(response.body); + assert.equal(body.results.length, 1); + assert.equal(body.results[0].tx_id, hex(0x1001)); + assert.deepEqual(body.cursor, { + next: null, + previous: null, + current: `1:${I32_MAX}:0`, + }); + }); + + test('should preserve exact height:0:0 transaction cursors', async () => { + await db.update( + new TestBlockBuilder({ + block_height: 1, + index_block_hash: hex(1), + parent_index_block_hash: hex(0), + parent_block_hash: hex(0), + }) + .addTx({ + tx_id: hex(0x1001), + tx_index: 0, + microblock_sequence: 0, + }) + .addTx({ + tx_id: hex(0x1002), + tx_index: 1, + microblock_sequence: I32_MAX, + }) + .build() + ); + + const response = await api.fastifyApp.inject({ + method: 'GET', + url: '/extended/v3/blocks/1/transactions', + query: { + limit: '1', + cursor: '1:0:0', + }, + }); + assert.equal(response.statusCode, 200); + const body = JSON.parse(response.body); + assert.equal(body.results.length, 1); + assert.equal(body.results[0].tx_id, hex(0x1001)); + assert.deepEqual(body.cursor, { + next: null, + previous: `1:${I32_MAX}:1`, + current: '1:0:0', + }); + }); + test('should return 304 when ETag matches and refresh ETag per block', async () => { await db.update( new TestBlockBuilder({ diff --git a/tests/api/v3/principals.test.ts b/tests/api/v3/principals.test.ts index c4dae9b09..3e9b4d54f 100644 --- a/tests/api/v3/principals.test.ts +++ b/tests/api/v3/principals.test.ts @@ -7,6 +7,7 @@ import * as assert from 'node:assert/strict'; import { TestBlockBuilder } from '../test-builders.ts'; import { DbTxStatus, DbTxTypeId } from '../../../src/datastore/common.ts'; import { hex } from '../test-helpers.ts'; +import { I32_MAX } from '../../../src/helpers.ts'; describe('principals', () => { let db: PgWriteStore; @@ -314,6 +315,120 @@ describe('principals', () => { previous: '10:0:4', current: '9:0:4', }); + + // Fetch a partial page that has fewer than `limit` newer rows. The previous + // cursor should still point back to the first available row. + const partialPreviousPage = await api.fastifyApp.inject({ + method: 'GET', + url: `/extended/v3/principals/${emptyPrincipal}/transactions`, + query: { + limit: '5', + cursor: '12:0:2', + }, + }); + assert.equal(partialPreviousPage.statusCode, 200); + const partialPreviousBody = JSON.parse(partialPreviousPage.body); + assert.equal(partialPreviousBody.results.length, 5); + assert.deepEqual(partialPreviousBody.cursor, { + next: '11:0:2', + previous: '12:0:4', + current: '12:0:2', + }); + }); + + test('should allow block-boundary cursors for anchored transactions', async () => { + await db.update( + new TestBlockBuilder({ + block_height: 3, + block_hash: hex(3), + index_block_hash: hex(3), + parent_index_block_hash: hex(2), + parent_block_hash: hex(2), + }) + .addTx({ + tx_id: hex(0x3001), + block_hash: hex(3), + index_block_hash: hex(3), + block_time: 3000, + burn_block_height: 3, + burn_block_time: 3000, + tx_index: 0, + microblock_sequence: I32_MAX, + sender_address: emptyPrincipal, + }) + .build() + ); + + const response = await api.fastifyApp.inject({ + method: 'GET', + url: `/extended/v3/principals/${emptyPrincipal}/transactions`, + query: { + limit: '1', + cursor: '3:0:0', + }, + }); + assert.equal(response.statusCode, 200); + const body = JSON.parse(response.body); + assert.equal(body.results.length, 1); + assert.equal(body.results[0].transaction.tx_id, hex(0x3001)); + assert.deepEqual(body.cursor, { + next: null, + previous: null, + current: `3:${I32_MAX}:0`, + }); + }); + + test('should preserve exact height:0:0 transaction cursors', async () => { + await db.update( + new TestBlockBuilder({ + block_height: 3, + block_hash: hex(3), + index_block_hash: hex(3), + parent_index_block_hash: hex(2), + parent_block_hash: hex(2), + }) + .addTx({ + tx_id: hex(0x3001), + block_hash: hex(3), + index_block_hash: hex(3), + block_time: 3000, + burn_block_height: 3, + burn_block_time: 3000, + tx_index: 0, + microblock_sequence: 0, + sender_address: emptyPrincipal, + }) + .addTx({ + tx_id: hex(0x3002), + block_hash: hex(3), + index_block_hash: hex(3), + block_time: 3000, + burn_block_height: 3, + burn_block_time: 3000, + tx_index: 1, + microblock_sequence: I32_MAX, + sender_address: emptyPrincipal, + }) + .build() + ); + + const response = await api.fastifyApp.inject({ + method: 'GET', + url: `/extended/v3/principals/${emptyPrincipal}/transactions`, + query: { + limit: '1', + cursor: '3:0:0', + }, + }); + assert.equal(response.statusCode, 200); + const body = JSON.parse(response.body); + assert.equal(body.results.length, 1); + assert.equal(body.results[0].transaction.tx_id, hex(0x3001)); + assert.deepEqual(body.cursor, { + next: null, + previous: `3:${I32_MAX}:1`, + current: '3:0:0', + }); }); test('should return 304 when ETag matches and refresh ETag on new principal activity', async () => { diff --git a/tests/api/v3/transactions.test.ts b/tests/api/v3/transactions.test.ts index 74c52e3f0..54f87e085 100644 --- a/tests/api/v3/transactions.test.ts +++ b/tests/api/v3/transactions.test.ts @@ -9,6 +9,7 @@ import { DbTxStatus, DbTxTypeId } from '../../../src/datastore/common.ts'; import { stringAsciiCV, uintCV } from '@stacks/transactions'; import { bufferToHex } from '@stacks/api-toolkit'; import { hex } from '../test-helpers.ts'; +import { I32_MAX } from '../../../src/helpers.ts'; describe('transactions', () => { let db: PgWriteStore; @@ -224,6 +225,80 @@ describe('transactions', () => { }); }); + test('should allow block-boundary cursors for anchored transactions', async () => { + await db.update( + new TestBlockBuilder({ + block_height: 1, + index_block_hash: hex(1), + parent_index_block_hash: hex(0), + parent_block_hash: hex(0), + }) + .addTx({ + tx_id: hex(0x1001), + microblock_sequence: I32_MAX, + }) + .build() + ); + + const response = await api.fastifyApp.inject({ + method: 'GET', + url: '/extended/v3/transactions', + query: { + limit: '1', + cursor: '1:0:0', + }, + }); + assert.equal(response.statusCode, 200); + const body = JSON.parse(response.body); + assert.equal(body.results.length, 1); + assert.equal(body.results[0].tx_id, hex(0x1001)); + assert.deepEqual(body.cursor, { + next: null, + previous: null, + current: `1:${I32_MAX}:0`, + }); + }); + + test('should preserve exact height:0:0 transaction cursors', async () => { + await db.update( + new TestBlockBuilder({ + block_height: 1, + index_block_hash: hex(1), + parent_index_block_hash: hex(0), + parent_block_hash: hex(0), + }) + .addTx({ + tx_id: hex(0x1001), + tx_index: 0, + microblock_sequence: 0, + }) + .addTx({ + tx_id: hex(0x1002), + tx_index: 1, + microblock_sequence: I32_MAX, + }) + .build() + ); + + const response = await api.fastifyApp.inject({ + method: 'GET', + url: '/extended/v3/transactions', + query: { + limit: '1', + cursor: '1:0:0', + }, + }); + assert.equal(response.statusCode, 200); + const body = JSON.parse(response.body); + assert.equal(body.results.length, 1); + assert.equal(body.results[0].tx_id, hex(0x1001)); + assert.deepEqual(body.cursor, { + next: null, + previous: `1:${I32_MAX}:1`, + current: '1:0:0', + }); + }); + test('should return 304 when ETag matches and refresh ETag when chain tip changes', async () => { await db.update( new TestBlockBuilder({ From 438db7258f2ff2315f6cad6fa42cee0a8423d3d4 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 26 May 2026 14:10:05 -0600 Subject: [PATCH 08/31] chore(release): 9.0.0-next.34 [skip ci] --- client/package-lock.json | 4 +- client/package.json | 2 +- client/src/generated/schema.d.ts | 1503 ++++++++++ openapi.yaml | 4461 +++++++++++++++++++++++++++++- package-lock.json | 4 +- package.json | 2 +- 6 files changed, 5969 insertions(+), 7 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index ade2fe7eb..869b0cf58 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.33", + "version": "9.0.0-next.34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.33", + "version": "9.0.0-next.34", "license": "GPL-3.0", "dependencies": { "@types/node": "20.14.14", diff --git a/client/package.json b/client/package.json index b7cb233c7..dd523c289 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.33", + "version": "9.0.0-next.34", "access": "public", "description": "Client for the Stacks Blockchain API", "homepage": "https://github.com/hirosystems/stacks-blockchain-api/tree/master/client#readme", diff --git a/client/src/generated/schema.d.ts b/client/src/generated/schema.d.ts index e6a1808fe..a58f20947 100644 --- a/client/src/generated/schema.d.ts +++ b/client/src/generated/schema.d.ts @@ -1730,6 +1730,26 @@ export interface paths { patch?: never; trace?: never; }; + "/extended/v3/transactions/{tx_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get transaction + * @description Retrieves details for a given transaction, including both mined and mempool transactions + */ + get: operations["get_transaction"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/extended/v3/mempool/transactions": { parameters: { query?: never; @@ -32390,6 +32410,1489 @@ export interface operations { }; }; }; + get_transaction: { + parameters: { + query?: { + /** @description Heavy fields to include in the response. Omitted by default to keep the payload lean. Provide as repeated querystring values (`?include=A&include=B`) or as a single comma-separated value (`?include=A,B`). */ + include?: ("function_args" | "source_code" | "post_conditions" | "result")[]; + }; + header?: never; + path: { + /** + * @description Transaction ID + * @example 0xf6bd5f4a7b26184a3466340b2e99fd003b4962c0e382a7e4b6a13df3dd7a91c6 + */ + tx_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Default Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": ({ + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + block: { + /** @description Height of the block this transactions was associated with */ + height: number; + /** @description Hash of the blocked this transactions was associated with */ + hash: string; + /** @description Hash of the index block this transactions was associated with */ + index_hash: string; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + /** @description Index of the transaction, indicating the order. Starts at `0` and increases with each transaction */ + tx_index: number; + }; + bitcoin_block: { + /** @description Height of the anchor burn block. */ + height: number; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + }; + /** @description Status of the transaction */ + status: "success" | "abort_by_response" | "abort_by_post_condition"; + parent_block: { + /** @description Hash of the parent block */ + hash: string; + /** @description Index block hash of the parent block */ + index_hash: string; + }; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + /** @description Number of events in the transaction */ + event_count: number; + execution_cost: { + /** @description Number of reads in the transaction */ + read_count: number; + /** @description Length of reads in the transaction */ + read_length: number; + /** @description Runtime of the transaction */ + runtime: number; + /** @description Number of writes in the transaction */ + write_count: number; + /** @description Length of writes in the transaction */ + write_length: number; + }; + vm_error: string | null; + result?: { + hex: string; + repr: string; + }; + /** @enum {string} */ + type: "token_transfer"; + token_transfer: { + /** @description Recipient of the token transfer */ + recipient: string; + /** @description Amount of the token transfer */ + amount: string; + memo: string | null; + }; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + block: { + /** @description Height of the block this transactions was associated with */ + height: number; + /** @description Hash of the blocked this transactions was associated with */ + hash: string; + /** @description Hash of the index block this transactions was associated with */ + index_hash: string; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + /** @description Index of the transaction, indicating the order. Starts at `0` and increases with each transaction */ + tx_index: number; + }; + bitcoin_block: { + /** @description Height of the anchor burn block. */ + height: number; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + }; + /** @description Status of the transaction */ + status: "success" | "abort_by_response" | "abort_by_post_condition"; + parent_block: { + /** @description Hash of the parent block */ + hash: string; + /** @description Index block hash of the parent block */ + index_hash: string; + }; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + /** @description Number of events in the transaction */ + event_count: number; + execution_cost: { + /** @description Number of reads in the transaction */ + read_count: number; + /** @description Length of reads in the transaction */ + read_length: number; + /** @description Runtime of the transaction */ + runtime: number; + /** @description Number of writes in the transaction */ + write_count: number; + /** @description Length of writes in the transaction */ + write_length: number; + }; + vm_error: string | null; + result?: { + hex: string; + repr: string; + }; + /** @enum {string} */ + type: "smart_contract"; + smart_contract: { + /** @description Contract ID of the smart contract */ + contract_id: string; + clarity_version: number | null; + /** @description Source code of the smart contract. Only present when requested via the `include=source_code` query param. */ + source_code?: string; + }; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + block: { + /** @description Height of the block this transactions was associated with */ + height: number; + /** @description Hash of the blocked this transactions was associated with */ + hash: string; + /** @description Hash of the index block this transactions was associated with */ + index_hash: string; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + /** @description Index of the transaction, indicating the order. Starts at `0` and increases with each transaction */ + tx_index: number; + }; + bitcoin_block: { + /** @description Height of the anchor burn block. */ + height: number; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + }; + /** @description Status of the transaction */ + status: "success" | "abort_by_response" | "abort_by_post_condition"; + parent_block: { + /** @description Hash of the parent block */ + hash: string; + /** @description Index block hash of the parent block */ + index_hash: string; + }; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + /** @description Number of events in the transaction */ + event_count: number; + execution_cost: { + /** @description Number of reads in the transaction */ + read_count: number; + /** @description Length of reads in the transaction */ + read_length: number; + /** @description Runtime of the transaction */ + runtime: number; + /** @description Number of writes in the transaction */ + write_count: number; + /** @description Length of writes in the transaction */ + write_length: number; + }; + vm_error: string | null; + result?: { + hex: string; + repr: string; + }; + /** @enum {string} */ + type: "contract_call"; + contract_call: { + /** @description Contract ID of the contract call */ + contract_id: string; + /** @description Function name of the contract call */ + function_name: string; + /** @description List of arguments used to invoke the function. Only present when requested via the `include=function_args` query param. */ + function_args?: { + hex: string; + repr: string; + }[]; + }; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + block: { + /** @description Height of the block this transactions was associated with */ + height: number; + /** @description Hash of the blocked this transactions was associated with */ + hash: string; + /** @description Hash of the index block this transactions was associated with */ + index_hash: string; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + /** @description Index of the transaction, indicating the order. Starts at `0` and increases with each transaction */ + tx_index: number; + }; + bitcoin_block: { + /** @description Height of the anchor burn block. */ + height: number; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + }; + /** @description Status of the transaction */ + status: "success" | "abort_by_response" | "abort_by_post_condition"; + parent_block: { + /** @description Hash of the parent block */ + hash: string; + /** @description Index block hash of the parent block */ + index_hash: string; + }; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + /** @description Number of events in the transaction */ + event_count: number; + execution_cost: { + /** @description Number of reads in the transaction */ + read_count: number; + /** @description Length of reads in the transaction */ + read_length: number; + /** @description Runtime of the transaction */ + runtime: number; + /** @description Number of writes in the transaction */ + write_count: number; + /** @description Length of writes in the transaction */ + write_length: number; + }; + vm_error: string | null; + result?: { + hex: string; + repr: string; + }; + /** @enum {string} */ + type: "poison_microblock"; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + block: { + /** @description Height of the block this transactions was associated with */ + height: number; + /** @description Hash of the blocked this transactions was associated with */ + hash: string; + /** @description Hash of the index block this transactions was associated with */ + index_hash: string; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + /** @description Index of the transaction, indicating the order. Starts at `0` and increases with each transaction */ + tx_index: number; + }; + bitcoin_block: { + /** @description Height of the anchor burn block. */ + height: number; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + }; + /** @description Status of the transaction */ + status: "success" | "abort_by_response" | "abort_by_post_condition"; + parent_block: { + /** @description Hash of the parent block */ + hash: string; + /** @description Index block hash of the parent block */ + index_hash: string; + }; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + /** @description Number of events in the transaction */ + event_count: number; + execution_cost: { + /** @description Number of reads in the transaction */ + read_count: number; + /** @description Length of reads in the transaction */ + read_length: number; + /** @description Runtime of the transaction */ + runtime: number; + /** @description Number of writes in the transaction */ + write_count: number; + /** @description Length of writes in the transaction */ + write_length: number; + }; + vm_error: string | null; + result?: { + hex: string; + repr: string; + }; + /** @enum {string} */ + type: "tenure_change"; + tenure_change: { + /** @description Consensus hash of this tenure. Corresponds to the sortition in which the miner of this block was chosen. */ + tenure_consensus_hash: string; + /** @description Consensus hash of the previous tenure. Corresponds to the sortition of the previous winning block-commit. */ + prev_tenure_consensus_hash: string; + /** @description Current consensus hash on the underlying burnchain. Corresponds to the last-seen sortition. */ + burn_view_consensus_hash: string; + /** @description (Hex string) Stacks Block hash */ + previous_tenure_end: string; + /** @description The number of blocks produced in the previous tenure. */ + previous_tenure_blocks: number; + /** @description Cause of change in mining tenure. Depending on cause, tenure can be ended or extended. */ + cause: "block_found" | "extended" | "extended_runtime" | "extended_read_count" | "extended_read_length" | "extended_write_count" | "extended_write_length"; + /** @description (Hex string) The ECDSA public key hash of the current tenure. */ + pubkey_hash: string; + }; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + block: { + /** @description Height of the block this transactions was associated with */ + height: number; + /** @description Hash of the blocked this transactions was associated with */ + hash: string; + /** @description Hash of the index block this transactions was associated with */ + index_hash: string; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + /** @description Index of the transaction, indicating the order. Starts at `0` and increases with each transaction */ + tx_index: number; + }; + bitcoin_block: { + /** @description Height of the anchor burn block. */ + height: number; + /** @description Unix timestamp (in seconds) indicating when this block was mined. */ + time: number; + }; + /** @description Status of the transaction */ + status: "success" | "abort_by_response" | "abort_by_post_condition"; + parent_block: { + /** @description Hash of the parent block */ + hash: string; + /** @description Index block hash of the parent block */ + index_hash: string; + }; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + /** @description Number of events in the transaction */ + event_count: number; + execution_cost: { + /** @description Number of reads in the transaction */ + read_count: number; + /** @description Length of reads in the transaction */ + read_length: number; + /** @description Runtime of the transaction */ + runtime: number; + /** @description Number of writes in the transaction */ + write_count: number; + /** @description Length of writes in the transaction */ + write_length: number; + }; + vm_error: string | null; + result?: { + hex: string; + repr: string; + }; + /** @enum {string} */ + type: "coinbase"; + coinbase: { + /** @description Payload of the coinbase transaction */ + payload: string; + alt_recipient: string | null; + vrf_proof: string | null; + }; + }) | ({ + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + /** @description A unix timestamp (in seconds) indicating when the transaction broadcast was received by the node. */ + receipt_time: number; + /** @description Height of the block this transaction was received by the node */ + receipt_block_height: number; + /** @description Status of the mempool transaction */ + status: "pending" | "dropped_replace_by_fee" | "dropped_replace_across_fork" | "dropped_too_expensive" | "dropped_stale_garbage_collect" | "dropped_problematic"; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + replaced_by_tx_id: string | null; + /** @enum {string} */ + type: "token_transfer"; + token_transfer: { + /** @description Recipient of the token transfer */ + recipient: string; + /** @description Amount of the token transfer */ + amount: string; + memo: string | null; + }; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + /** @description A unix timestamp (in seconds) indicating when the transaction broadcast was received by the node. */ + receipt_time: number; + /** @description Height of the block this transaction was received by the node */ + receipt_block_height: number; + /** @description Status of the mempool transaction */ + status: "pending" | "dropped_replace_by_fee" | "dropped_replace_across_fork" | "dropped_too_expensive" | "dropped_stale_garbage_collect" | "dropped_problematic"; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + replaced_by_tx_id: string | null; + /** @enum {string} */ + type: "smart_contract"; + smart_contract: { + /** @description Contract ID of the smart contract */ + contract_id: string; + clarity_version: number | null; + /** @description Source code of the smart contract. Only present when requested via the `include=source_code` query param. */ + source_code?: string; + }; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + /** @description A unix timestamp (in seconds) indicating when the transaction broadcast was received by the node. */ + receipt_time: number; + /** @description Height of the block this transaction was received by the node */ + receipt_block_height: number; + /** @description Status of the mempool transaction */ + status: "pending" | "dropped_replace_by_fee" | "dropped_replace_across_fork" | "dropped_too_expensive" | "dropped_stale_garbage_collect" | "dropped_problematic"; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + replaced_by_tx_id: string | null; + /** @enum {string} */ + type: "contract_call"; + contract_call: { + /** @description Contract ID of the contract call */ + contract_id: string; + /** @description Function name of the contract call */ + function_name: string; + /** @description List of arguments used to invoke the function. Only present when requested via the `include=function_args` query param. */ + function_args?: { + hex: string; + repr: string; + }[]; + }; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + /** @description A unix timestamp (in seconds) indicating when the transaction broadcast was received by the node. */ + receipt_time: number; + /** @description Height of the block this transaction was received by the node */ + receipt_block_height: number; + /** @description Status of the mempool transaction */ + status: "pending" | "dropped_replace_by_fee" | "dropped_replace_across_fork" | "dropped_too_expensive" | "dropped_stale_garbage_collect" | "dropped_problematic"; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + replaced_by_tx_id: string | null; + /** @enum {string} */ + type: "poison_microblock"; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + /** @description A unix timestamp (in seconds) indicating when the transaction broadcast was received by the node. */ + receipt_time: number; + /** @description Height of the block this transaction was received by the node */ + receipt_block_height: number; + /** @description Status of the mempool transaction */ + status: "pending" | "dropped_replace_by_fee" | "dropped_replace_across_fork" | "dropped_too_expensive" | "dropped_stale_garbage_collect" | "dropped_problematic"; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + replaced_by_tx_id: string | null; + /** @enum {string} */ + type: "tenure_change"; + } | { + /** @description Transaction ID */ + tx_id: string; + sender: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + }; + sponsor: { + /** @description Address of the transaction initiator */ + address: string; + /** @description Nonce of the transaction initiator */ + nonce: number; + } | null; + /** @description Transaction fee as Integer string (64-bit unsigned integer). */ + fee_rate: string; + /** @description A unix timestamp (in seconds) indicating when the transaction broadcast was received by the node. */ + receipt_time: number; + /** @description Height of the block this transaction was received by the node */ + receipt_block_height: number; + /** @description Status of the mempool transaction */ + status: "pending" | "dropped_replace_by_fee" | "dropped_replace_across_fork" | "dropped_too_expensive" | "dropped_stale_garbage_collect" | "dropped_problematic"; + /** @description Only present when requested via the `include=post_conditions` query param. */ + post_conditions?: ({ + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "stx"; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent_equal_to" | "sent_greater_than" | "sent_greater_than_or_equal_to" | "sent_less_than" | "sent_less_than_or_equal_to"; + amount: string; + /** @enum {string} */ + type: "fungible"; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + } | { + principal: { + /** @enum {string} */ + type_id: "principal_origin"; + } | { + /** @enum {string} */ + type_id: "principal_standard"; + address: string; + } | { + /** @enum {string} */ + type_id: "principal_contract"; + address: string; + contract_name: string; + }; + condition_code: "sent" | "not_sent" | "maybe_sent"; + /** @enum {string} */ + type: "non_fungible"; + asset_value: { + hex: string; + repr: string; + }; + asset: { + asset_name: string; + contract_address: string; + contract_name: string; + }; + })[]; + replaced_by_tx_id: string | null; + /** @enum {string} */ + type: "coinbase"; + }); + }; + }; + /** @description Default Response */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + message?: string; + } & { + [key: string]: unknown; + }; + }; + }; + }; + }; get_mempool_transactions: { parameters: { query?: { diff --git a/openapi.yaml b/openapi.yaml index bfdbee068..f90138bd3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4,7 +4,7 @@ info: description: Welcome to the API reference overview for the [Stacks Blockchain API](https://docs.hiro.so/stacks-blockchain-api). [Download Postman collection](https://hirosystems.github.io/stacks-blockchain-api/collection.json). - version: 9.0.0-next.33 + version: 9.0.0-next.34 components: schemas: {} paths: @@ -93358,6 +93358,4465 @@ paths: type: string message: type: string + /extended/v3/transactions/{tx_id}: + get: + operationId: get_transaction + summary: Get transaction + tags: + - Transactions + description: Retrieves details for a given transaction, including both mined and + mempool transactions + parameters: + - schema: + uniqueItems: true + type: array + items: + anyOf: + - type: string + enum: + - function_args + - type: string + enum: + - source_code + - type: string + enum: + - post_conditions + - type: string + enum: + - result + in: query + name: include + required: false + description: Heavy fields to include in the response. Omitted by default to keep + the payload lean. Provide as repeated querystring values + (`?include=A&include=B`) or as a single comma-separated value + (`?include=A,B`). + - schema: + pattern: ^(0x)?[a-fA-F0-9]{64}$ + title: Transaction ID + type: string + example: "0xf6bd5f4a7b26184a3466340b2e99fd003b4962c0e382a7e4b6a13df3dd7a91c6" + in: path + name: tx_id + required: true + description: Transaction ID + responses: + "200": + description: Default Response + content: + application/json: + schema: + anyOf: + - anyOf: + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - block + - bitcoin_block + - status + - parent_block + - event_count + - execution_cost + - vm_error + - type + - token_transfer + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + block: + type: object + required: + - height + - hash + - index_hash + - time + - tx_index + properties: + height: + description: Height of the block this transactions was associated with + type: integer + hash: + description: Hash of the blocked this transactions was associated with + type: string + index_hash: + description: Hash of the index block this transactions was associated with + type: string + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + tx_index: + description: Index of the transaction, indicating the order. Starts at `0` and + increases with each transaction + type: integer + bitcoin_block: + type: object + required: + - height + - time + properties: + height: + description: Height of the anchor burn block. + type: integer + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + status: + description: Status of the transaction + anyOf: + - type: string + enum: + - success + - type: string + enum: + - abort_by_response + - type: string + enum: + - abort_by_post_condition + parent_block: + type: object + required: + - hash + - index_hash + properties: + hash: + description: Hash of the parent block + type: string + index_hash: + description: Index block hash of the parent block + type: string + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + event_count: + description: Number of events in the transaction + type: integer + execution_cost: + type: object + required: + - read_count + - read_length + - runtime + - write_count + - write_length + properties: + read_count: + description: Number of reads in the transaction + type: integer + read_length: + description: Length of reads in the transaction + type: integer + runtime: + description: Runtime of the transaction + type: integer + write_count: + description: Number of writes in the transaction + type: integer + write_length: + description: Length of writes in the transaction + type: integer + vm_error: + anyOf: + - description: VM error of the transaction + type: string + - type: "null" + result: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + type: + type: string + enum: + - token_transfer + token_transfer: + type: object + required: + - recipient + - amount + - memo + properties: + recipient: + description: Recipient of the token transfer + type: string + amount: + description: Amount of the token transfer + type: string + memo: + anyOf: + - description: Memo of the token transfer + type: string + - type: "null" + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - block + - bitcoin_block + - status + - parent_block + - event_count + - execution_cost + - vm_error + - type + - smart_contract + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + block: + type: object + required: + - height + - hash + - index_hash + - time + - tx_index + properties: + height: + description: Height of the block this transactions was associated with + type: integer + hash: + description: Hash of the blocked this transactions was associated with + type: string + index_hash: + description: Hash of the index block this transactions was associated with + type: string + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + tx_index: + description: Index of the transaction, indicating the order. Starts at `0` and + increases with each transaction + type: integer + bitcoin_block: + type: object + required: + - height + - time + properties: + height: + description: Height of the anchor burn block. + type: integer + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + status: + description: Status of the transaction + anyOf: + - type: string + enum: + - success + - type: string + enum: + - abort_by_response + - type: string + enum: + - abort_by_post_condition + parent_block: + type: object + required: + - hash + - index_hash + properties: + hash: + description: Hash of the parent block + type: string + index_hash: + description: Index block hash of the parent block + type: string + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + event_count: + description: Number of events in the transaction + type: integer + execution_cost: + type: object + required: + - read_count + - read_length + - runtime + - write_count + - write_length + properties: + read_count: + description: Number of reads in the transaction + type: integer + read_length: + description: Length of reads in the transaction + type: integer + runtime: + description: Runtime of the transaction + type: integer + write_count: + description: Number of writes in the transaction + type: integer + write_length: + description: Length of writes in the transaction + type: integer + vm_error: + anyOf: + - description: VM error of the transaction + type: string + - type: "null" + result: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + type: + type: string + enum: + - smart_contract + smart_contract: + type: object + required: + - contract_id + - clarity_version + properties: + contract_id: + description: Contract ID of the smart contract + type: string + clarity_version: + anyOf: + - description: Clarity version of the smart contract + type: number + - type: "null" + source_code: + description: Source code of the smart contract. Only present when requested via + the `include=source_code` query param. + type: string + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - block + - bitcoin_block + - status + - parent_block + - event_count + - execution_cost + - vm_error + - type + - contract_call + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + block: + type: object + required: + - height + - hash + - index_hash + - time + - tx_index + properties: + height: + description: Height of the block this transactions was associated with + type: integer + hash: + description: Hash of the blocked this transactions was associated with + type: string + index_hash: + description: Hash of the index block this transactions was associated with + type: string + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + tx_index: + description: Index of the transaction, indicating the order. Starts at `0` and + increases with each transaction + type: integer + bitcoin_block: + type: object + required: + - height + - time + properties: + height: + description: Height of the anchor burn block. + type: integer + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + status: + description: Status of the transaction + anyOf: + - type: string + enum: + - success + - type: string + enum: + - abort_by_response + - type: string + enum: + - abort_by_post_condition + parent_block: + type: object + required: + - hash + - index_hash + properties: + hash: + description: Hash of the parent block + type: string + index_hash: + description: Index block hash of the parent block + type: string + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + event_count: + description: Number of events in the transaction + type: integer + execution_cost: + type: object + required: + - read_count + - read_length + - runtime + - write_count + - write_length + properties: + read_count: + description: Number of reads in the transaction + type: integer + read_length: + description: Length of reads in the transaction + type: integer + runtime: + description: Runtime of the transaction + type: integer + write_count: + description: Number of writes in the transaction + type: integer + write_length: + description: Length of writes in the transaction + type: integer + vm_error: + anyOf: + - description: VM error of the transaction + type: string + - type: "null" + result: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + type: + type: string + enum: + - contract_call + contract_call: + type: object + required: + - contract_id + - function_name + properties: + contract_id: + description: Contract ID of the contract call + type: string + function_name: + description: Function name of the contract call + type: string + function_args: + description: List of arguments used to invoke the function. Only present when + requested via the `include=function_args` + query param. + type: array + items: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - block + - bitcoin_block + - status + - parent_block + - event_count + - execution_cost + - vm_error + - type + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + block: + type: object + required: + - height + - hash + - index_hash + - time + - tx_index + properties: + height: + description: Height of the block this transactions was associated with + type: integer + hash: + description: Hash of the blocked this transactions was associated with + type: string + index_hash: + description: Hash of the index block this transactions was associated with + type: string + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + tx_index: + description: Index of the transaction, indicating the order. Starts at `0` and + increases with each transaction + type: integer + bitcoin_block: + type: object + required: + - height + - time + properties: + height: + description: Height of the anchor burn block. + type: integer + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + status: + description: Status of the transaction + anyOf: + - type: string + enum: + - success + - type: string + enum: + - abort_by_response + - type: string + enum: + - abort_by_post_condition + parent_block: + type: object + required: + - hash + - index_hash + properties: + hash: + description: Hash of the parent block + type: string + index_hash: + description: Index block hash of the parent block + type: string + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + event_count: + description: Number of events in the transaction + type: integer + execution_cost: + type: object + required: + - read_count + - read_length + - runtime + - write_count + - write_length + properties: + read_count: + description: Number of reads in the transaction + type: integer + read_length: + description: Length of reads in the transaction + type: integer + runtime: + description: Runtime of the transaction + type: integer + write_count: + description: Number of writes in the transaction + type: integer + write_length: + description: Length of writes in the transaction + type: integer + vm_error: + anyOf: + - description: VM error of the transaction + type: string + - type: "null" + result: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + type: + type: string + enum: + - poison_microblock + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - block + - bitcoin_block + - status + - parent_block + - event_count + - execution_cost + - vm_error + - type + - tenure_change + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + block: + type: object + required: + - height + - hash + - index_hash + - time + - tx_index + properties: + height: + description: Height of the block this transactions was associated with + type: integer + hash: + description: Hash of the blocked this transactions was associated with + type: string + index_hash: + description: Hash of the index block this transactions was associated with + type: string + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + tx_index: + description: Index of the transaction, indicating the order. Starts at `0` and + increases with each transaction + type: integer + bitcoin_block: + type: object + required: + - height + - time + properties: + height: + description: Height of the anchor burn block. + type: integer + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + status: + description: Status of the transaction + anyOf: + - type: string + enum: + - success + - type: string + enum: + - abort_by_response + - type: string + enum: + - abort_by_post_condition + parent_block: + type: object + required: + - hash + - index_hash + properties: + hash: + description: Hash of the parent block + type: string + index_hash: + description: Index block hash of the parent block + type: string + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + event_count: + description: Number of events in the transaction + type: integer + execution_cost: + type: object + required: + - read_count + - read_length + - runtime + - write_count + - write_length + properties: + read_count: + description: Number of reads in the transaction + type: integer + read_length: + description: Length of reads in the transaction + type: integer + runtime: + description: Runtime of the transaction + type: integer + write_count: + description: Number of writes in the transaction + type: integer + write_length: + description: Length of writes in the transaction + type: integer + vm_error: + anyOf: + - description: VM error of the transaction + type: string + - type: "null" + result: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + type: + type: string + enum: + - tenure_change + tenure_change: + type: object + required: + - tenure_consensus_hash + - prev_tenure_consensus_hash + - burn_view_consensus_hash + - previous_tenure_end + - previous_tenure_blocks + - cause + - pubkey_hash + properties: + tenure_consensus_hash: + description: Consensus hash of this tenure. Corresponds to the sortition in + which the miner of this block was chosen. + type: string + prev_tenure_consensus_hash: + description: Consensus hash of the previous tenure. Corresponds to the sortition + of the previous winning block-commit. + type: string + burn_view_consensus_hash: + description: Current consensus hash on the underlying burnchain. Corresponds to + the last-seen sortition. + type: string + previous_tenure_end: + description: (Hex string) Stacks Block hash + type: string + previous_tenure_blocks: + description: The number of blocks produced in the previous tenure. + type: integer + cause: + description: Cause of change in mining tenure. Depending on cause, tenure can be + ended or extended. + anyOf: + - type: string + enum: + - block_found + - type: string + enum: + - extended + - type: string + enum: + - extended_runtime + - type: string + enum: + - extended_read_count + - type: string + enum: + - extended_read_length + - type: string + enum: + - extended_write_count + - type: string + enum: + - extended_write_length + pubkey_hash: + description: (Hex string) The ECDSA public key hash of the current tenure. + type: string + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - block + - bitcoin_block + - status + - parent_block + - event_count + - execution_cost + - vm_error + - type + - coinbase + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + block: + type: object + required: + - height + - hash + - index_hash + - time + - tx_index + properties: + height: + description: Height of the block this transactions was associated with + type: integer + hash: + description: Hash of the blocked this transactions was associated with + type: string + index_hash: + description: Hash of the index block this transactions was associated with + type: string + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + tx_index: + description: Index of the transaction, indicating the order. Starts at `0` and + increases with each transaction + type: integer + bitcoin_block: + type: object + required: + - height + - time + properties: + height: + description: Height of the anchor burn block. + type: integer + time: + description: Unix timestamp (in seconds) indicating when this block was mined. + type: number + status: + description: Status of the transaction + anyOf: + - type: string + enum: + - success + - type: string + enum: + - abort_by_response + - type: string + enum: + - abort_by_post_condition + parent_block: + type: object + required: + - hash + - index_hash + properties: + hash: + description: Hash of the parent block + type: string + index_hash: + description: Index block hash of the parent block + type: string + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + event_count: + description: Number of events in the transaction + type: integer + execution_cost: + type: object + required: + - read_count + - read_length + - runtime + - write_count + - write_length + properties: + read_count: + description: Number of reads in the transaction + type: integer + read_length: + description: Length of reads in the transaction + type: integer + runtime: + description: Runtime of the transaction + type: integer + write_count: + description: Number of writes in the transaction + type: integer + write_length: + description: Length of writes in the transaction + type: integer + vm_error: + anyOf: + - description: VM error of the transaction + type: string + - type: "null" + result: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + type: + type: string + enum: + - coinbase + coinbase: + type: object + required: + - payload + - alt_recipient + - vrf_proof + properties: + payload: + description: Payload of the coinbase transaction + type: string + alt_recipient: + anyOf: + - description: Alt recipient of the coinbase transaction + type: string + - type: "null" + vrf_proof: + anyOf: + - description: VRF proof of the coinbase transaction + type: string + - type: "null" + - anyOf: + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - receipt_time + - receipt_block_height + - status + - replaced_by_tx_id + - type + - token_transfer + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + receipt_time: + description: A unix timestamp (in seconds) indicating when the transaction + broadcast was received by the node. + type: integer + receipt_block_height: + description: Height of the block this transaction was received by the node + type: integer + status: + description: Status of the mempool transaction + anyOf: + - type: string + enum: + - pending + - type: string + enum: + - dropped_replace_by_fee + - type: string + enum: + - dropped_replace_across_fork + - type: string + enum: + - dropped_too_expensive + - type: string + enum: + - dropped_stale_garbage_collect + - type: string + enum: + - dropped_problematic + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + replaced_by_tx_id: + anyOf: + - description: ID of another transaction which replaced this one + type: string + - type: "null" + type: + type: string + enum: + - token_transfer + token_transfer: + type: object + required: + - recipient + - amount + - memo + properties: + recipient: + description: Recipient of the token transfer + type: string + amount: + description: Amount of the token transfer + type: string + memo: + anyOf: + - description: Memo of the token transfer + type: string + - type: "null" + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - receipt_time + - receipt_block_height + - status + - replaced_by_tx_id + - type + - smart_contract + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + receipt_time: + description: A unix timestamp (in seconds) indicating when the transaction + broadcast was received by the node. + type: integer + receipt_block_height: + description: Height of the block this transaction was received by the node + type: integer + status: + description: Status of the mempool transaction + anyOf: + - type: string + enum: + - pending + - type: string + enum: + - dropped_replace_by_fee + - type: string + enum: + - dropped_replace_across_fork + - type: string + enum: + - dropped_too_expensive + - type: string + enum: + - dropped_stale_garbage_collect + - type: string + enum: + - dropped_problematic + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + replaced_by_tx_id: + anyOf: + - description: ID of another transaction which replaced this one + type: string + - type: "null" + type: + type: string + enum: + - smart_contract + smart_contract: + type: object + required: + - contract_id + - clarity_version + properties: + contract_id: + description: Contract ID of the smart contract + type: string + clarity_version: + anyOf: + - description: Clarity version of the smart contract + type: number + - type: "null" + source_code: + description: Source code of the smart contract. Only present when requested via + the `include=source_code` query param. + type: string + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - receipt_time + - receipt_block_height + - status + - replaced_by_tx_id + - type + - contract_call + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + receipt_time: + description: A unix timestamp (in seconds) indicating when the transaction + broadcast was received by the node. + type: integer + receipt_block_height: + description: Height of the block this transaction was received by the node + type: integer + status: + description: Status of the mempool transaction + anyOf: + - type: string + enum: + - pending + - type: string + enum: + - dropped_replace_by_fee + - type: string + enum: + - dropped_replace_across_fork + - type: string + enum: + - dropped_too_expensive + - type: string + enum: + - dropped_stale_garbage_collect + - type: string + enum: + - dropped_problematic + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + replaced_by_tx_id: + anyOf: + - description: ID of another transaction which replaced this one + type: string + - type: "null" + type: + type: string + enum: + - contract_call + contract_call: + type: object + required: + - contract_id + - function_name + properties: + contract_id: + description: Contract ID of the contract call + type: string + function_name: + description: Function name of the contract call + type: string + function_args: + description: List of arguments used to invoke the function. Only present when + requested via the `include=function_args` + query param. + type: array + items: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - receipt_time + - receipt_block_height + - status + - replaced_by_tx_id + - type + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + receipt_time: + description: A unix timestamp (in seconds) indicating when the transaction + broadcast was received by the node. + type: integer + receipt_block_height: + description: Height of the block this transaction was received by the node + type: integer + status: + description: Status of the mempool transaction + anyOf: + - type: string + enum: + - pending + - type: string + enum: + - dropped_replace_by_fee + - type: string + enum: + - dropped_replace_across_fork + - type: string + enum: + - dropped_too_expensive + - type: string + enum: + - dropped_stale_garbage_collect + - type: string + enum: + - dropped_problematic + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + replaced_by_tx_id: + anyOf: + - description: ID of another transaction which replaced this one + type: string + - type: "null" + type: + type: string + enum: + - poison_microblock + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - receipt_time + - receipt_block_height + - status + - replaced_by_tx_id + - type + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + receipt_time: + description: A unix timestamp (in seconds) indicating when the transaction + broadcast was received by the node. + type: integer + receipt_block_height: + description: Height of the block this transaction was received by the node + type: integer + status: + description: Status of the mempool transaction + anyOf: + - type: string + enum: + - pending + - type: string + enum: + - dropped_replace_by_fee + - type: string + enum: + - dropped_replace_across_fork + - type: string + enum: + - dropped_too_expensive + - type: string + enum: + - dropped_stale_garbage_collect + - type: string + enum: + - dropped_problematic + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + replaced_by_tx_id: + anyOf: + - description: ID of another transaction which replaced this one + type: string + - type: "null" + type: + type: string + enum: + - tenure_change + - type: object + required: + - tx_id + - sender + - sponsor + - fee_rate + - receipt_time + - receipt_block_height + - status + - replaced_by_tx_id + - type + properties: + tx_id: + description: Transaction ID + type: string + sender: + type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + sponsor: + anyOf: + - type: object + required: + - address + - nonce + properties: + address: + description: Address of the transaction initiator + type: string + nonce: + description: Nonce of the transaction initiator + type: integer + - type: "null" + fee_rate: + description: Transaction fee as Integer string (64-bit unsigned integer). + type: string + receipt_time: + description: A unix timestamp (in seconds) indicating when the transaction + broadcast was received by the node. + type: integer + receipt_block_height: + description: Height of the block this transaction was received by the node + type: integer + status: + description: Status of the mempool transaction + anyOf: + - type: string + enum: + - pending + - type: string + enum: + - dropped_replace_by_fee + - type: string + enum: + - dropped_replace_across_fork + - type: string + enum: + - dropped_too_expensive + - type: string + enum: + - dropped_stale_garbage_collect + - type: string + enum: + - dropped_problematic + post_conditions: + description: Only present when requested via the `include=post_conditions` query + param. + type: array + items: + anyOf: + - type: object + required: + - principal + - condition_code + - amount + - type + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - stx + - type: object + required: + - principal + - condition_code + - amount + - type + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent_equal_to + - type: string + enum: + - sent_greater_than + - type: string + enum: + - sent_greater_than_or_equal_to + - type: string + enum: + - sent_less_than + - type: string + enum: + - sent_less_than_or_equal_to + amount: + type: string + type: + type: string + enum: + - fungible + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + - type: object + required: + - principal + - condition_code + - type + - asset_value + - asset + properties: + principal: + anyOf: + - type: object + required: + - type_id + properties: + type_id: + type: string + enum: + - principal_origin + - type: object + required: + - type_id + - address + properties: + type_id: + type: string + enum: + - principal_standard + address: + type: string + - type: object + required: + - type_id + - address + - contract_name + properties: + type_id: + type: string + enum: + - principal_contract + address: + type: string + contract_name: + type: string + condition_code: + anyOf: + - type: string + enum: + - sent + - type: string + enum: + - not_sent + - type: string + enum: + - maybe_sent + type: + type: string + enum: + - non_fungible + asset_value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + asset: + type: object + required: + - asset_name + - contract_address + - contract_name + properties: + asset_name: + type: string + contract_address: + type: string + contract_name: + type: string + replaced_by_tx_id: + anyOf: + - description: ID of another transaction which replaced this one + type: string + - type: "null" + type: + type: string + enum: + - coinbase + 4XX: + description: Default Response + content: + application/json: + schema: + title: Error Response + additionalProperties: true + type: object + required: + - error + properties: + error: + type: string + message: + type: string /extended/v3/mempool/transactions: get: operationId: get_mempool_transactions diff --git a/package-lock.json b/package-lock.json index 96494dca4..e5446676b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.33", + "version": "9.0.0-next.34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.33", + "version": "9.0.0-next.34", "license": "GPL-3.0", "dependencies": { "@fastify/cors": "11.2.0", diff --git a/package.json b/package.json index 5d7ab996e..e02d24542 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.33", + "version": "9.0.0-next.34", "type": "module", "main": "./lib/index.js", "scripts": { From 8802e36518bf8c3b31726c5e847113995d07eb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20C=C3=A1rdenas?= <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 27 May 2026 13:41:52 -0600 Subject: [PATCH 09/31] feat: add v3 transaction events endpoint (#2566) * events progress * adjust event schemas * query and indexes * add tests * fix clarity memo * fix other tests --- ...75100000000_tx-event-pagination-indexes.ts | 49 +++++ src/api/routes/v3/transactions.ts | 44 +++++ src/api/schemas/v3/cursors.ts | 6 + src/api/schemas/v3/entities/common.ts | 17 ++ .../schemas/v3/entities/transaction-events.ts | 134 +++++++++++++ .../v3/entities/transaction-summaries.ts | 8 +- src/api/serializers/v3/transaction-events.ts | 186 ++++++++++++++++++ src/api/serializers/v3/transactions.ts | 7 +- src/datastore/v3/pg-store-v3.ts | 149 +++++++++++++- src/datastore/v3/types.ts | 17 +- tests/api/v3/blocks.test.ts | 7 +- tests/api/v3/mempool.test.ts | 5 +- tests/api/v3/principals.test.ts | 7 +- tests/api/v3/transactions.test.ts | 147 +++++++++++++- 14 files changed, 767 insertions(+), 16 deletions(-) create mode 100644 migrations/1775100000000_tx-event-pagination-indexes.ts create mode 100644 src/api/schemas/v3/entities/transaction-events.ts create mode 100644 src/api/serializers/v3/transaction-events.ts diff --git a/migrations/1775100000000_tx-event-pagination-indexes.ts b/migrations/1775100000000_tx-event-pagination-indexes.ts new file mode 100644 index 000000000..6e6d36c05 --- /dev/null +++ b/migrations/1775100000000_tx-event-pagination-indexes.ts @@ -0,0 +1,49 @@ +import type { MigrationBuilder } from 'node-pg-migrate'; + +const CANONICAL_EVENT_WHERE = 'canonical = TRUE AND microblock_canonical = TRUE'; + +export const up = (pgm: MigrationBuilder) => { + pgm.createIndex('stx_events', ['tx_id', 'event_index'], { + name: 'stx_events_canonical_tx_id_event_index_idx', + where: CANONICAL_EVENT_WHERE, + }); + pgm.createIndex('ft_events', ['tx_id', 'event_index'], { + name: 'ft_events_canonical_tx_id_event_index_idx', + where: CANONICAL_EVENT_WHERE, + }); + pgm.createIndex('nft_events', ['tx_id', 'event_index'], { + name: 'nft_events_canonical_tx_id_event_index_idx', + where: CANONICAL_EVENT_WHERE, + }); + pgm.createIndex('stx_lock_events', ['tx_id', 'event_index'], { + name: 'stx_lock_events_canonical_tx_id_event_index_idx', + where: CANONICAL_EVENT_WHERE, + }); + pgm.createIndex('contract_logs', ['tx_id', 'event_index'], { + name: 'contract_logs_canonical_tx_id_event_index_idx', + where: CANONICAL_EVENT_WHERE, + }); + + // Redundant after adding (tx_id, event_index) indexes above. + pgm.dropIndex('stx_events', 'tx_id'); + pgm.dropIndex('ft_events', 'tx_id'); + pgm.dropIndex('nft_events', 'tx_id'); + pgm.dropIndex('stx_lock_events', 'tx_id'); + pgm.dropIndex('contract_logs', 'tx_id'); +}; + +export const down = (pgm: MigrationBuilder) => { + pgm.createIndex('stx_events', 'tx_id'); + pgm.createIndex('ft_events', 'tx_id'); + pgm.createIndex('nft_events', 'tx_id'); + pgm.createIndex('stx_lock_events', 'tx_id'); + pgm.createIndex('contract_logs', 'tx_id'); + + pgm.dropIndex('stx_events', [], { name: 'stx_events_canonical_tx_id_event_index_idx' }); + pgm.dropIndex('ft_events', [], { name: 'ft_events_canonical_tx_id_event_index_idx' }); + pgm.dropIndex('nft_events', [], { name: 'nft_events_canonical_tx_id_event_index_idx' }); + pgm.dropIndex('stx_lock_events', [], { + name: 'stx_lock_events_canonical_tx_id_event_index_idx', + }); + pgm.dropIndex('contract_logs', [], { name: 'contract_logs_canonical_tx_id_event_index_idx' }); +}; diff --git a/src/api/routes/v3/transactions.ts b/src/api/routes/v3/transactions.ts index 43521f2bb..2a8903913 100644 --- a/src/api/routes/v3/transactions.ts +++ b/src/api/routes/v3/transactions.ts @@ -12,6 +12,7 @@ import { CursorPaginatedResponse, CursorPaginationQuerystring, TransactionCursorSchema, + TransactionEventCursorSchema, } from '../../schemas/v3/cursors.js'; import { TransactionIdSchema } from '../../schemas/v3/entities/common.js'; import { @@ -20,6 +21,8 @@ import { } from '../../schemas/v3/entities/transactions.js'; import { MempoolTransactionSchema } from '../../schemas/v3/entities/mempool-transactions.js'; import { NotFoundError } from '../../../errors.js'; +import { TransactionEventSchema } from '../../schemas/v3/entities/transaction-events.js'; +import { serializeDbTransactionEvent } from '../../serializers/v3/transaction-events.js'; export const TransactionsRoutes: FastifyPluginAsync< Record, @@ -116,5 +119,46 @@ export const TransactionsRoutes: FastifyPluginAsync< } ); + fastify.get( + '/transactions/:tx_id/events', + { + preHandler: handleTransactionCache, + schema: { + operationId: 'get_transaction_events', + summary: 'Get transaction events', + description: `Retrieves events for a given transaction ID`, + tags: ['Transactions'], + params: Type.Object({ + tx_id: TransactionIdSchema, + }), + querystring: CursorPaginationQuerystring(TransactionEventCursorSchema, ResourceType.Event), + response: { + 200: CursorPaginatedResponse( + TransactionEventSchema, + TransactionEventCursorSchema, + ResourceType.Event + ), + }, + }, + }, + async (req, reply) => { + const events = await fastify.db.v3.getTransactionEvents({ + txId: req.params.tx_id, + limit: getPagingQueryLimit(ResourceType.Event, req.query.limit), + cursor: req.query.cursor, + }); + await reply.send({ + total: events.total, + limit: events.limit, + cursor: { + next: events.next_cursor, + previous: events.prev_cursor, + current: events.current_cursor, + }, + results: events.results.map(r => serializeDbTransactionEvent(r)), + }); + } + ); + await Promise.resolve(); }; diff --git a/src/api/schemas/v3/cursors.ts b/src/api/schemas/v3/cursors.ts index d8271aad4..0f68ba2ef 100644 --- a/src/api/schemas/v3/cursors.ts +++ b/src/api/schemas/v3/cursors.ts @@ -65,3 +65,9 @@ export const MempoolTransactionCursorSchema = Type.String({ description: 'Cursor for paginating mempool transactions. Format: receipt_time:tx_id', }); export type MempoolTransactionCursor = Static; + +export const TransactionEventCursorSchema = Type.String({ + pattern: '^[0-9]+$', + description: 'Cursor for paginating transaction events. Format: event_index', +}); +export type TransactionEventCursor = Static; diff --git a/src/api/schemas/v3/entities/common.ts b/src/api/schemas/v3/entities/common.ts index 3690e6bf3..16356e6fe 100644 --- a/src/api/schemas/v3/entities/common.ts +++ b/src/api/schemas/v3/entities/common.ts @@ -19,6 +19,15 @@ export type SmartContractId = Static; export const PrincipalSchema = Type.Union([AddressSchema, SmartContractIdSchema]); export type Principal = Static; +export const AssetIdentifierSchema = Type.String({ + pattern: + '^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}::[a-zA-Z-]+$', + title: 'Asset Identifier', + description: 'Asset Identifier', + examples: ['SP000000000000000000002Q6VF78.pox-3::stx-token'], +}); +export type AssetIdentifier = Static; + export const TransactionIdSchema = Type.String({ pattern: '^(0x)?[a-fA-F0-9]{64}$', title: 'Transaction ID', @@ -75,3 +84,11 @@ export const ExecutionCostSchema = Type.Object({ }), }); export type ExecutionCost = Static; + +export const AmountSchema = Type.String({ + pattern: '^[0-9]+$', + title: 'Amount', + description: 'Amount', + examples: ['1000000'], +}); +export type Amount = Static; diff --git a/src/api/schemas/v3/entities/transaction-events.ts b/src/api/schemas/v3/entities/transaction-events.ts new file mode 100644 index 000000000..9f14e72d3 --- /dev/null +++ b/src/api/schemas/v3/entities/transaction-events.ts @@ -0,0 +1,134 @@ +import { Static, Type } from '@sinclair/typebox'; +import { + AssetIdentifierSchema, + DecodedClarityValueSchema, + PrincipalSchema, + SmartContractIdSchema, + BlockHeightSchema, + AmountSchema, +} from './common.js'; +import { Nullable } from '../../v1/util.js'; + +const BaseTransactionEventSchema = Type.Object({ + event_index: Type.Integer(), +}); + +export const ContractLogTransactionEventSchema = Type.Composite([ + BaseTransactionEventSchema, + Type.Object({ + type: Type.Literal('contract_log'), + contract_log: Type.Object({ + contract_id: SmartContractIdSchema, + topic: Type.Literal('print'), + value: DecodedClarityValueSchema, + }), + }), +]); +export type ContractLogTransactionEvent = Static; + +const StxLockTransactionEventSchema = Type.Composite([ + BaseTransactionEventSchema, + Type.Object({ + type: Type.Literal('stx_lock'), + stx_lock: Type.Object({ + amount: AmountSchema, + unlock_bitcoin_height: BlockHeightSchema, + address: PrincipalSchema, + }), + }), +]); +export type StxLockTransactionEvent = Static; + +const StxTransactionEventSchema = Type.Composite([ + BaseTransactionEventSchema, + Type.Object({ + type: Type.Literal('stx_asset'), + stx_asset: Type.Union([ + Type.Object({ + type: Type.Literal('transfer'), + sender: PrincipalSchema, + recipient: PrincipalSchema, + amount: AmountSchema, + memo: Nullable(DecodedClarityValueSchema), + }), + Type.Object({ + type: Type.Literal('mint'), + recipient: PrincipalSchema, + amount: AmountSchema, + }), + Type.Object({ + type: Type.Literal('burn'), + sender: PrincipalSchema, + amount: AmountSchema, + }), + ]), + }), +]); +export type StxTransactionEvent = Static; + +export const FtTransactionEventSchema = Type.Composite([ + BaseTransactionEventSchema, + Type.Object({ + type: Type.Literal('ft_asset'), + ft_asset: Type.Union([ + Type.Object({ + type: Type.Literal('transfer'), + asset_identifier: AssetIdentifierSchema, + sender: PrincipalSchema, + recipient: PrincipalSchema, + amount: AmountSchema, + }), + Type.Object({ + type: Type.Literal('mint'), + recipient: PrincipalSchema, + asset_identifier: AssetIdentifierSchema, + amount: AmountSchema, + }), + Type.Object({ + type: Type.Literal('burn'), + sender: PrincipalSchema, + asset_identifier: AssetIdentifierSchema, + amount: AmountSchema, + }), + ]), + }), +]); +export type FtTransactionEvent = Static; + +export const NftTransactionEventSchema = Type.Composite([ + BaseTransactionEventSchema, + Type.Object({ + type: Type.Literal('nft_asset'), + nft_asset: Type.Union([ + Type.Object({ + type: Type.Literal('transfer'), + asset_identifier: AssetIdentifierSchema, + sender: PrincipalSchema, + recipient: PrincipalSchema, + value: DecodedClarityValueSchema, + }), + Type.Object({ + type: Type.Literal('mint'), + recipient: PrincipalSchema, + asset_identifier: AssetIdentifierSchema, + value: DecodedClarityValueSchema, + }), + Type.Object({ + type: Type.Literal('burn'), + sender: PrincipalSchema, + asset_identifier: AssetIdentifierSchema, + value: DecodedClarityValueSchema, + }), + ]), + }), +]); +export type NftTransactionEvent = Static; + +export const TransactionEventSchema = Type.Union([ + ContractLogTransactionEventSchema, + StxLockTransactionEventSchema, + StxTransactionEventSchema, + FtTransactionEventSchema, + NftTransactionEventSchema, +]); +export type TransactionEvent = Static; diff --git a/src/api/schemas/v3/entities/transaction-summaries.ts b/src/api/schemas/v3/entities/transaction-summaries.ts index 23a2e7460..8d33e5509 100644 --- a/src/api/schemas/v3/entities/transaction-summaries.ts +++ b/src/api/schemas/v3/entities/transaction-summaries.ts @@ -1,5 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { Nullable } from '../../v1/util.js'; +import { DecodedClarityValueSchema } from './common.js'; export const TransactionSenderSchema = Type.Object({ address: Type.String({ @@ -87,12 +88,7 @@ export const TokenTransferTransactionSummarySchema = Type.Composite( amount: Type.String({ description: 'Transfer amount as Integer string (64-bit unsigned integer)', }), - memo: Nullable( - Type.String({ - description: - 'Hex encoded arbitrary message, up to 34 bytes length (should try decoding to an ASCII string)', - }) - ), + memo: Nullable(DecodedClarityValueSchema), }), }), ], diff --git a/src/api/serializers/v3/transaction-events.ts b/src/api/serializers/v3/transaction-events.ts new file mode 100644 index 000000000..9d26c3796 --- /dev/null +++ b/src/api/serializers/v3/transaction-events.ts @@ -0,0 +1,186 @@ +import { TransactionEvent } from '../../schemas/v3/entities/transaction-events.js'; +import { DbTransactionEvent } from '../../../datastore/v3/types.js'; +import { DbAssetEventTypeId, DbEventTypeId } from 'src/datastore/common.js'; +import { decodeClarityValueToRepr } from '@stacks/codec'; + +/** + * Serializes a database transaction event into a transaction event. + * @param event - The database transaction event to serialize. + * @returns The serialized transaction event. + */ +export function serializeDbTransactionEvent(event: DbTransactionEvent): TransactionEvent { + switch (event.event_type_id) { + case DbEventTypeId.SmartContractLog: { + return { + event_index: event.event_index, + type: 'contract_log', + contract_log: { + contract_id: event.contract_identifier!, + topic: 'print', + value: { + hex: event.value!, + repr: decodeClarityValueToRepr(event.value!), + }, + }, + }; + } + case DbEventTypeId.StxAsset: { + switch (event.asset_event_type_id) { + case DbAssetEventTypeId.Transfer: { + return { + event_index: event.event_index, + type: 'stx_asset', + stx_asset: { + type: 'transfer', + sender: event.sender!, + recipient: event.recipient!, + amount: event.amount, + memo: event.memo + ? { + hex: event.memo, + repr: decodeClarityValueToRepr(event.memo), + } + : null, + }, + }; + } + case DbAssetEventTypeId.Mint: { + return { + event_index: event.event_index, + type: 'stx_asset', + stx_asset: { + type: 'mint', + recipient: event.recipient!, + amount: event.amount, + }, + }; + } + case DbAssetEventTypeId.Burn: { + return { + event_index: event.event_index, + type: 'stx_asset', + stx_asset: { + type: 'burn', + sender: event.sender!, + amount: event.amount, + }, + }; + } + default: { + throw new Error(`Unexpected asset_event_type_id in: ${JSON.stringify(event)}`); + } + } + } + case DbEventTypeId.FungibleTokenAsset: { + switch (event.asset_event_type_id) { + case DbAssetEventTypeId.Transfer: { + return { + event_index: event.event_index, + type: 'ft_asset', + ft_asset: { + type: 'transfer', + asset_identifier: event.asset_identifier!, + sender: event.sender!, + recipient: event.recipient!, + amount: event.amount, + }, + }; + } + case DbAssetEventTypeId.Mint: { + return { + event_index: event.event_index, + type: 'ft_asset', + ft_asset: { + type: 'mint', + recipient: event.recipient!, + asset_identifier: event.asset_identifier!, + amount: event.amount, + }, + }; + } + case DbAssetEventTypeId.Burn: { + return { + event_index: event.event_index, + type: 'ft_asset', + ft_asset: { + type: 'burn', + sender: event.sender!, + asset_identifier: event.asset_identifier!, + amount: event.amount, + }, + }; + } + default: { + throw new Error(`Unexpected asset_event_type_id in: ${JSON.stringify(event)}`); + } + } + } + case DbEventTypeId.NonFungibleTokenAsset: { + switch (event.asset_event_type_id) { + case DbAssetEventTypeId.Transfer: { + return { + event_index: event.event_index, + type: 'nft_asset', + nft_asset: { + type: 'transfer', + asset_identifier: event.asset_identifier!, + sender: event.sender!, + recipient: event.recipient!, + value: { + hex: event.value!, + repr: decodeClarityValueToRepr(event.value!), + }, + }, + }; + } + case DbAssetEventTypeId.Mint: { + return { + event_index: event.event_index, + type: 'nft_asset', + nft_asset: { + type: 'mint', + recipient: event.recipient!, + asset_identifier: event.asset_identifier!, + value: { + hex: event.value!, + repr: decodeClarityValueToRepr(event.value!), + }, + }, + }; + } + case DbAssetEventTypeId.Burn: { + return { + event_index: event.event_index, + type: 'nft_asset', + nft_asset: { + type: 'burn', + sender: event.sender!, + asset_identifier: event.asset_identifier!, + value: { + hex: event.value!, + repr: decodeClarityValueToRepr(event.value!), + }, + }, + }; + } + default: { + throw new Error(`Unexpected asset_event_type_id in: ${JSON.stringify(event)}`); + } + } + } + case DbEventTypeId.StxLock: { + return { + event_index: event.event_index, + type: 'stx_lock', + stx_lock: { + amount: event.amount, + unlock_bitcoin_height: event.unlock_height!, + address: event.sender!, + }, + }; + } + default: { + throw new Error(`Unexpected event_type_id in: ${JSON.stringify(event)}`); + } + } +} diff --git a/src/api/serializers/v3/transactions.ts b/src/api/serializers/v3/transactions.ts index 587de28e8..d7b46a4bd 100644 --- a/src/api/serializers/v3/transactions.ts +++ b/src/api/serializers/v3/transactions.ts @@ -123,7 +123,12 @@ export function serializeDbTransactionSummary(summary: DbTransactionSummary): Tr token_transfer: { recipient: summary.token_transfer_recipient_address!, amount: summary.token_transfer_amount!, - memo: summary.token_transfer_memo, + memo: summary.token_transfer_memo + ? { + hex: summary.token_transfer_memo, + repr: decodeClarityValueToRepr(summary.token_transfer_memo), + } + : null, }, }; return tokenTransfer; diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index c6b52a405..3fb4a9503 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -5,6 +5,7 @@ import { DbMempoolTransactionSummary, DbPrincipalTransactionSummary, DbTransaction, + DbTransactionEvent, DbTransactionSummary, } from './types.js'; import { @@ -19,8 +20,9 @@ import { normalizeHashString } from '../../helpers.js'; import { BlockIdParam } from '../../api/routes/v2/schemas.js'; import { InvalidRequestError, InvalidRequestErrorType } from '../../errors.js'; import { TransactionIncludeField } from '../../api/schemas/v3/entities/transactions.js'; -import type { TransactionCursor } from '../../api/schemas/v3/cursors.js'; +import type { TransactionCursor, TransactionEventCursor } from '../../api/schemas/v3/cursors.js'; import { encodeTransactionCursor, resolveTransactionCursor } from './helpers.js'; +import { DbEventTypeId } from '../common.js'; export class PgStoreV3 extends BasePgStoreModule { /** @@ -494,4 +496,149 @@ export class PgStoreV3 extends BasePgStoreModule { return null; }); } + + async getTransactionEvents(args: { + txId: string; + limit: number; + cursor?: TransactionEventCursor; + }): Promise> { + return await this.sqlTransaction(async sql => { + const limit = args.limit; + const txCheck = await sql<{ event_count: number }[]>` + SELECT event_count + FROM txs + WHERE tx_id = ${args.txId} AND canonical = true AND microblock_canonical = true + LIMIT 1 + `; + if (txCheck.count === 0) + throw new InvalidRequestError( + `Transaction not found`, + InvalidRequestErrorType.invalid_param + ); + + let cursorFilter = sql``; + if (args.cursor) { + cursorFilter = sql`AND event_index >= ${parseInt(args.cursor, 10)}`; + } + + const eventCond = sql` + canonical = true AND microblock_canonical = true AND tx_id = ${args.txId} ${cursorFilter} + `; + const resultQuery = await sql` + WITH events AS ( + ( + SELECT + sender, + recipient, + event_index, + amount, + NULL as asset_identifier, + NULL as contract_identifier, + NULL as topic, + NULL::bytea as value, + ${DbEventTypeId.StxAsset}::int as event_type_id, + asset_event_type_id, + memo, + NULL::int as unlock_height + FROM stx_events + WHERE ${eventCond} + ) + UNION ALL + ( + SELECT + sender, + recipient, + event_index, + amount, + asset_identifier, + NULL as contract_identifier, + NULL as topic, + NULL::bytea as value, + ${DbEventTypeId.FungibleTokenAsset}::int as event_type_id, + asset_event_type_id, + NULL::bytea as memo, + NULL::int as unlock_height + FROM ft_events + WHERE ${eventCond} + ) + UNION ALL + ( + SELECT + sender, + recipient, + event_index, + 0 as amount, + asset_identifier, + NULL as contract_identifier, + NULL as topic, + value, + ${DbEventTypeId.NonFungibleTokenAsset}::int as event_type_id, + asset_event_type_id, + NULL::bytea as memo, + NULL::int as unlock_height + FROM nft_events + WHERE ${eventCond} + ) + UNION ALL + ( + SELECT + locked_address as sender, + NULL as recipient, + event_index, + locked_amount as amount, + NULL as asset_identifier, + NULL as contract_identifier, + NULL as topic, + NULL::bytea as value, + ${DbEventTypeId.StxLock}::int as event_type_id, + 0 as asset_event_type_id, + NULL::bytea as memo, + unlock_height + FROM stx_lock_events + WHERE ${eventCond} + ) + UNION ALL + ( + SELECT + NULL as sender, + NULL as recipient, + event_index, + 0 as amount, + NULL as asset_identifier, + contract_identifier, + topic, + value, + ${DbEventTypeId.SmartContractLog}::int as event_type_id, + 0 as asset_event_type_id, + NULL::bytea as memo, + NULL::int as unlock_height + FROM contract_logs + WHERE ${eventCond} + ) + ) + SELECT * + FROM events + ORDER BY event_index ASC + LIMIT ${limit + 1} + `; + const hasNextPage = resultQuery.count > limit; + const results = hasNextPage ? resultQuery.slice(0, limit) : resultQuery; + const firstResult = results[0]; + const extraResult = hasNextPage ? resultQuery[limit] : null; + const prevCursor = + firstResult && firstResult.event_index > 0 + ? Math.max(firstResult.event_index - limit, 0).toString() + : null; + + return { + total: txCheck[0].event_count, + limit, + offset: 0, + next_cursor: extraResult ? extraResult.event_index.toString() : null, + prev_cursor: prevCursor, + current_cursor: firstResult ? firstResult.event_index.toString() : null, + results, + }; + }); + } } diff --git a/src/datastore/v3/types.ts b/src/datastore/v3/types.ts index 818e9b8f5..8b00c7d47 100644 --- a/src/datastore/v3/types.ts +++ b/src/datastore/v3/types.ts @@ -1,4 +1,4 @@ -import { DbTxStatus, DbTxTypeId } from '../common.js'; +import { DbAssetEventTypeId, DbEventTypeId, DbTxStatus, DbTxTypeId } from '../common.js'; export type DbCursorPaginatedResult = { limit: number; @@ -108,3 +108,18 @@ export interface DbMempoolTransaction extends DbMempoolTransactionSummary { tenure_change_previous_tenure_blocks: number | null; tenure_change_pubkey_hash: string | null; } + +export interface DbTransactionEvent { + event_index: number; + amount: string; + event_type_id: DbEventTypeId; + asset_event_type_id: DbAssetEventTypeId; + sender: string | null; + recipient: string | null; + asset_identifier: string | null; + contract_identifier: string | null; + topic: string | null; + value: string | null; + memo: string | null; + unlock_height: number | null; +} diff --git a/tests/api/v3/blocks.test.ts b/tests/api/v3/blocks.test.ts index 880ae2547..39cd0faae 100644 --- a/tests/api/v3/blocks.test.ts +++ b/tests/api/v3/blocks.test.ts @@ -123,7 +123,7 @@ describe('blocks', () => { sender_address: SENDER, token_transfer_recipient_address: RECIPIENT, token_transfer_amount: 100n, - token_transfer_memo: '0x', + token_transfer_memo: '0x0d0000000568656c6c6f', }) .build() ); @@ -167,7 +167,10 @@ describe('blocks', () => { token_transfer: { recipient: RECIPIENT, amount: '100', - memo: '0x', + memo: { + hex: '0x0d0000000568656c6c6f', + repr: '"hello"', + }, }, }); assert.deepEqual(body.results[1], { diff --git a/tests/api/v3/mempool.test.ts b/tests/api/v3/mempool.test.ts index 112b4e55e..13df00b14 100644 --- a/tests/api/v3/mempool.test.ts +++ b/tests/api/v3/mempool.test.ts @@ -72,7 +72,7 @@ describe('mempool', () => { sender_address: 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27', token_transfer_recipient_address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', token_transfer_amount: 500n, - token_transfer_memo: '0x', + token_transfer_memo: '0x0d0000000568656c6c6f', fee_rate: 250n, nonce: 3, }), @@ -133,7 +133,7 @@ describe('mempool', () => { token_transfer: { recipient: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', amount: '500', - memo: '0x', + memo: '0x0d0000000568656c6c6f', }, }); }); @@ -544,6 +544,7 @@ describe('mempool', () => { sender_address: SENDER, nonce: 1, type_id: DbTxTypeId.TokenTransfer, + token_transfer_memo: '0x0d0000000568656c6c6f', }) .build() ); diff --git a/tests/api/v3/principals.test.ts b/tests/api/v3/principals.test.ts index 3e9b4d54f..18c588c49 100644 --- a/tests/api/v3/principals.test.ts +++ b/tests/api/v3/principals.test.ts @@ -82,6 +82,7 @@ describe('principals', () => { status: DbTxStatus.Success, sender_address: sender, nonce: indexIdIndex, + token_transfer_memo: '0x0d0000000568656c6c6f', }); for (let i = 0; i < stxEventCount; i++) { block.addTxStxEvent({ @@ -178,7 +179,10 @@ describe('principals', () => { token_transfer: { recipient: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', amount: '100', - memo: '0x', + memo: { + hex: '0x0d0000000568656c6c6f', + repr: '"hello"', + }, }, }, involvement: 'sender', @@ -490,6 +494,7 @@ describe('principals', () => { status: DbTxStatus.Success, sender_address: testAddr1, nonce: 100, + token_transfer_memo: '0x0d0000000568656c6c6f', }) .build() ); diff --git a/tests/api/v3/transactions.test.ts b/tests/api/v3/transactions.test.ts index 54f87e085..4f16724a3 100644 --- a/tests/api/v3/transactions.test.ts +++ b/tests/api/v3/transactions.test.ts @@ -91,7 +91,7 @@ describe('transactions', () => { sender_address: 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27', token_transfer_recipient_address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', token_transfer_amount: 100n, - token_transfer_memo: '0x', + token_transfer_memo: '0x0d0000000568656c6c6f', }) .build() ); @@ -132,7 +132,10 @@ describe('transactions', () => { token_transfer: { recipient: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', amount: '100', - memo: '0x', + memo: { + hex: '0x0d0000000568656c6c6f', + repr: '"hello"', + }, }, }); assert.deepEqual(body.results[1], { @@ -554,4 +557,144 @@ describe('transactions', () => { assert.notEqual(otherTx.headers['etag'], etag); }); }); + + describe('/v3/transactions/:tx_id/events', () => { + const sender = 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27'; + const recipient = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6'; + + test('should return transaction events sorted by event_index across event tables', async () => { + const txId = hex(0x7001); + await db.update( + new TestBlockBuilder({ + block_height: 1, + index_block_hash: hex(1), + parent_index_block_hash: hex(0), + parent_block_hash: hex(0), + }) + .addTx({ + tx_id: txId, + tx_index: 0, + event_count: 5, + }) + .addTxStxEvent({ amount: 101n }) + .addTxFtEvent({ amount: 202n, sender, recipient }) + .addTxNftEvent({ sender, recipient }) + .addTxStxLockEvent({ locked_amount: 303, unlock_height: 555, locked_address: sender }) + .addTxContractLogEvent() + .build() + ); + + const response = await api.fastifyApp.inject({ + method: 'GET', + url: `/extended/v3/transactions/${txId}/events`, + }); + assert.equal(response.statusCode, 200); + const body = JSON.parse(response.body); + + assert.equal(body.total, 5); + assert.equal(body.limit, 20); + assert.deepEqual(body.cursor, { + next: null, + previous: null, + current: '0', + }); + assert.equal(body.results.length, 5); + assert.deepEqual( + body.results.map((event: { event_index: number }) => event.event_index), + [0, 1, 2, 3, 4] + ); + assert.deepEqual( + body.results.map((event: { type: string }) => event.type), + ['stx_asset', 'ft_asset', 'nft_asset', 'stx_lock', 'contract_log'] + ); + }); + + test('should cursor paginate transaction events by event_index', async () => { + const txId = hex(0x7002); + await db.update( + new TestBlockBuilder({ + block_height: 1, + index_block_hash: hex(1), + parent_index_block_hash: hex(0), + parent_block_hash: hex(0), + }) + .addTx({ + tx_id: txId, + tx_index: 0, + event_count: 5, + }) + .addTxStxEvent() + .addTxFtEvent({ sender, recipient }) + .addTxNftEvent({ sender, recipient }) + .addTxStxLockEvent({ locked_address: sender }) + .addTxContractLogEvent() + .build() + ); + + const page1 = await api.fastifyApp.inject({ + method: 'GET', + url: `/extended/v3/transactions/${txId}/events`, + query: { + limit: '2', + }, + }); + assert.equal(page1.statusCode, 200); + const body1 = JSON.parse(page1.body); + assert.equal(body1.total, 5); + assert.equal(body1.results.length, 2); + assert.deepEqual( + body1.results.map((event: { event_index: number }) => event.event_index), + [0, 1] + ); + assert.deepEqual(body1.cursor, { + next: '2', + previous: null, + current: '0', + }); + + const page2 = await api.fastifyApp.inject({ + method: 'GET', + url: `/extended/v3/transactions/${txId}/events`, + query: { + limit: '2', + cursor: '2', + }, + }); + assert.equal(page2.statusCode, 200); + const body2 = JSON.parse(page2.body); + assert.equal(body2.total, 5); + assert.equal(body2.results.length, 2); + assert.deepEqual( + body2.results.map((event: { event_index: number }) => event.event_index), + [2, 3] + ); + assert.deepEqual(body2.cursor, { + next: '4', + previous: '0', + current: '2', + }); + + const page3 = await api.fastifyApp.inject({ + method: 'GET', + url: `/extended/v3/transactions/${txId}/events`, + query: { + limit: '2', + cursor: '4', + }, + }); + assert.equal(page3.statusCode, 200); + const body3 = JSON.parse(page3.body); + assert.equal(body3.total, 5); + assert.equal(body3.results.length, 1); + assert.deepEqual( + body3.results.map((event: { event_index: number }) => event.event_index), + [4] + ); + assert.deepEqual(body3.cursor, { + next: null, + previous: '2', + current: '4', + }); + }); + }); }); From e39a882df55b603eae8c51f53bd593ffa8607f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20C=C3=A1rdenas?= <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 27 May 2026 20:23:20 -0600 Subject: [PATCH 10/31] fix: only modify event indexes if they dont exist (#2567) * fix: only modify event indexes if they dont exist * remove index removal --- ...75100000000_tx-event-pagination-indexes.ts | 39 +++++++++++-------- src/api/serializers/v3/transaction-events.ts | 2 +- src/datastore/pg-store.ts | 10 ++--- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/migrations/1775100000000_tx-event-pagination-indexes.ts b/migrations/1775100000000_tx-event-pagination-indexes.ts index 6e6d36c05..04f4d4141 100644 --- a/migrations/1775100000000_tx-event-pagination-indexes.ts +++ b/migrations/1775100000000_tx-event-pagination-indexes.ts @@ -6,44 +6,49 @@ export const up = (pgm: MigrationBuilder) => { pgm.createIndex('stx_events', ['tx_id', 'event_index'], { name: 'stx_events_canonical_tx_id_event_index_idx', where: CANONICAL_EVENT_WHERE, + ifNotExists: true, }); pgm.createIndex('ft_events', ['tx_id', 'event_index'], { name: 'ft_events_canonical_tx_id_event_index_idx', where: CANONICAL_EVENT_WHERE, + ifNotExists: true, }); pgm.createIndex('nft_events', ['tx_id', 'event_index'], { name: 'nft_events_canonical_tx_id_event_index_idx', where: CANONICAL_EVENT_WHERE, + ifNotExists: true, }); pgm.createIndex('stx_lock_events', ['tx_id', 'event_index'], { name: 'stx_lock_events_canonical_tx_id_event_index_idx', where: CANONICAL_EVENT_WHERE, + ifNotExists: true, }); pgm.createIndex('contract_logs', ['tx_id', 'event_index'], { name: 'contract_logs_canonical_tx_id_event_index_idx', where: CANONICAL_EVENT_WHERE, + ifNotExists: true, }); - - // Redundant after adding (tx_id, event_index) indexes above. - pgm.dropIndex('stx_events', 'tx_id'); - pgm.dropIndex('ft_events', 'tx_id'); - pgm.dropIndex('nft_events', 'tx_id'); - pgm.dropIndex('stx_lock_events', 'tx_id'); - pgm.dropIndex('contract_logs', 'tx_id'); }; export const down = (pgm: MigrationBuilder) => { - pgm.createIndex('stx_events', 'tx_id'); - pgm.createIndex('ft_events', 'tx_id'); - pgm.createIndex('nft_events', 'tx_id'); - pgm.createIndex('stx_lock_events', 'tx_id'); - pgm.createIndex('contract_logs', 'tx_id'); - - pgm.dropIndex('stx_events', [], { name: 'stx_events_canonical_tx_id_event_index_idx' }); - pgm.dropIndex('ft_events', [], { name: 'ft_events_canonical_tx_id_event_index_idx' }); - pgm.dropIndex('nft_events', [], { name: 'nft_events_canonical_tx_id_event_index_idx' }); + pgm.dropIndex('stx_events', [], { + name: 'stx_events_canonical_tx_id_event_index_idx', + ifExists: true, + }); + pgm.dropIndex('ft_events', [], { + name: 'ft_events_canonical_tx_id_event_index_idx', + ifExists: true, + }); + pgm.dropIndex('nft_events', [], { + name: 'nft_events_canonical_tx_id_event_index_idx', + ifExists: true, + }); pgm.dropIndex('stx_lock_events', [], { name: 'stx_lock_events_canonical_tx_id_event_index_idx', + ifExists: true, + }); + pgm.dropIndex('contract_logs', [], { + name: 'contract_logs_canonical_tx_id_event_index_idx', + ifExists: true, }); - pgm.dropIndex('contract_logs', [], { name: 'contract_logs_canonical_tx_id_event_index_idx' }); }; diff --git a/src/api/serializers/v3/transaction-events.ts b/src/api/serializers/v3/transaction-events.ts index 9d26c3796..b331d454b 100644 --- a/src/api/serializers/v3/transaction-events.ts +++ b/src/api/serializers/v3/transaction-events.ts @@ -1,6 +1,6 @@ import { TransactionEvent } from '../../schemas/v3/entities/transaction-events.js'; import { DbTransactionEvent } from '../../../datastore/v3/types.js'; -import { DbAssetEventTypeId, DbEventTypeId } from 'src/datastore/common.js'; +import { DbAssetEventTypeId, DbEventTypeId } from '../../../datastore/common.js'; import { decodeClarityValueToRepr } from '@stacks/codec'; /** diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 791719ae5..0ce550c54 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -1676,7 +1676,7 @@ export class PgStore extends BasePgStore { event_index, tx_id, tx_index, block_height, canonical, locked_amount, unlock_height, locked_address, contract_name FROM stx_lock_events WHERE tx_id = ${args.txId} AND index_block_hash = ${args.indexBlockHash} - AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} + AND canonical = true AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; const stxResults = await sql< { @@ -1696,7 +1696,7 @@ export class PgStore extends BasePgStore { event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, amount, memo FROM stx_events WHERE tx_id = ${args.txId} AND index_block_hash = ${args.indexBlockHash} - AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} + AND canonical = true AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; const ftResults = await sql< { @@ -1716,7 +1716,7 @@ export class PgStore extends BasePgStore { event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, asset_identifier, amount FROM ft_events WHERE tx_id = ${args.txId} AND index_block_hash = ${args.indexBlockHash} - AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} + AND canonical = true AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; const nftResults = await sql< { @@ -1736,7 +1736,7 @@ export class PgStore extends BasePgStore { event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, asset_identifier, value FROM nft_events WHERE tx_id = ${args.txId} AND index_block_hash = ${args.indexBlockHash} - AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} + AND canonical = true AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; const logResults = await sql< { @@ -1754,7 +1754,7 @@ export class PgStore extends BasePgStore { event_index, tx_id, tx_index, block_height, canonical, contract_identifier, topic, value FROM contract_logs WHERE tx_id = ${args.txId} AND index_block_hash = ${args.indexBlockHash} - AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} + AND canonical = true AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; return { results: parseDbEvents(stxLockResults, stxResults, ftResults, nftResults, logResults), From a86f09266b75efc69316db0ce6f9324322a9ad5c Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 27 May 2026 20:24:32 -0600 Subject: [PATCH 11/31] chore(release): v9.0.0-next.35 [skip ci] --- client/package-lock.json | 4 +- client/package.json | 2 +- client/src/generated/schema.d.ts | 258 +++++++++++- openapi.yaml | 659 ++++++++++++++++++++++++++++++- package-lock.json | 4 +- package.json | 2 +- 6 files changed, 910 insertions(+), 19 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 869b0cf58..b5ef68e3f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.34", + "version": "9.0.0-next.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.34", + "version": "9.0.0-next.35", "license": "GPL-3.0", "dependencies": { "@types/node": "20.14.14", diff --git a/client/package.json b/client/package.json index dd523c289..47029f516 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.34", + "version": "9.0.0-next.35", "access": "public", "description": "Client for the Stacks Blockchain API", "homepage": "https://github.com/hirosystems/stacks-blockchain-api/tree/master/client#readme", diff --git a/client/src/generated/schema.d.ts b/client/src/generated/schema.d.ts index a58f20947..dc6072fff 100644 --- a/client/src/generated/schema.d.ts +++ b/client/src/generated/schema.d.ts @@ -1750,6 +1750,26 @@ export interface paths { patch?: never; trace?: never; }; + "/extended/v3/transactions/{tx_id}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get transaction events + * @description Retrieves events for a given transaction ID + */ + get: operations["get_transaction_events"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/extended/v3/mempool/transactions": { parameters: { query?: never; @@ -31843,7 +31863,10 @@ export interface operations { recipient: string; /** @description Transfer amount as Integer string (64-bit unsigned integer) */ amount: string; - memo: string | null; + memo: { + hex: string; + repr: string; + } | null; }; } | { /** @description Transaction ID */ @@ -32175,7 +32198,10 @@ export interface operations { recipient: string; /** @description Transfer amount as Integer string (64-bit unsigned integer) */ amount: string; - memo: string | null; + memo: { + hex: string; + repr: string; + } | null; }; } | { /** @description Transaction ID */ @@ -33893,6 +33919,229 @@ export interface operations { }; }; }; + get_transaction_events: { + parameters: { + query?: { + /** @description Number of results per page */ + limit?: number; + /** @description Cursor for paginating transaction events. Format: event_index */ + cursor?: string; + }; + header?: never; + path: { + /** + * @description Transaction ID + * @example 0xf6bd5f4a7b26184a3466340b2e99fd003b4962c0e382a7e4b6a13df3dd7a91c6 + */ + tx_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Default Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example 1 */ + total: number; + /** + * @description Number of results per page + * @default 20 + */ + limit: number; + cursor: { + next: string | null; + previous: string | null; + current: string | null; + }; + results: ({ + event_index: number; + /** @enum {string} */ + type: "contract_log"; + contract_log: { + /** + * Smart Contract ID + * @description Smart Contract ID + */ + contract_id: string; + /** @enum {string} */ + topic: "print"; + value: { + hex: string; + repr: string; + }; + }; + } | { + event_index: number; + /** @enum {string} */ + type: "stx_lock"; + stx_lock: { + /** + * Amount + * @description Amount + */ + amount: string; + /** + * Block height + * @description Block height + */ + unlock_bitcoin_height: number; + address: string; + }; + } | { + event_index: number; + /** @enum {string} */ + type: "stx_asset"; + stx_asset: { + /** @enum {string} */ + type: "transfer"; + sender: string; + recipient: string; + /** + * Amount + * @description Amount + */ + amount: string; + memo: { + hex: string; + repr: string; + } | null; + } | { + /** @enum {string} */ + type: "mint"; + recipient: string; + /** + * Amount + * @description Amount + */ + amount: string; + } | { + /** @enum {string} */ + type: "burn"; + sender: string; + /** + * Amount + * @description Amount + */ + amount: string; + }; + } | { + event_index: number; + /** @enum {string} */ + type: "ft_asset"; + ft_asset: { + /** @enum {string} */ + type: "transfer"; + /** + * Asset Identifier + * @description Asset Identifier + */ + asset_identifier: string; + sender: string; + recipient: string; + /** + * Amount + * @description Amount + */ + amount: string; + } | { + /** @enum {string} */ + type: "mint"; + recipient: string; + /** + * Asset Identifier + * @description Asset Identifier + */ + asset_identifier: string; + /** + * Amount + * @description Amount + */ + amount: string; + } | { + /** @enum {string} */ + type: "burn"; + sender: string; + /** + * Asset Identifier + * @description Asset Identifier + */ + asset_identifier: string; + /** + * Amount + * @description Amount + */ + amount: string; + }; + } | { + event_index: number; + /** @enum {string} */ + type: "nft_asset"; + nft_asset: { + /** @enum {string} */ + type: "transfer"; + /** + * Asset Identifier + * @description Asset Identifier + */ + asset_identifier: string; + sender: string; + recipient: string; + value: { + hex: string; + repr: string; + }; + } | { + /** @enum {string} */ + type: "mint"; + recipient: string; + /** + * Asset Identifier + * @description Asset Identifier + */ + asset_identifier: string; + value: { + hex: string; + repr: string; + }; + } | { + /** @enum {string} */ + type: "burn"; + sender: string; + /** + * Asset Identifier + * @description Asset Identifier + */ + asset_identifier: string; + value: { + hex: string; + repr: string; + }; + }; + })[]; + }; + }; + }; + /** @description Default Response */ + "4XX": { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + message?: string; + } & { + [key: string]: unknown; + }; + }; + }; + }; + }; get_mempool_transactions: { parameters: { query?: { @@ -34191,7 +34440,10 @@ export interface operations { recipient: string; /** @description Transfer amount as Integer string (64-bit unsigned integer) */ amount: string; - memo: string | null; + memo: { + hex: string; + repr: string; + } | null; }; } | { /** @description Transaction ID */ diff --git a/openapi.yaml b/openapi.yaml index f90138bd3..02db6b05d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4,7 +4,7 @@ info: description: Welcome to the API reference overview for the [Stacks Blockchain API](https://docs.hiro.so/stacks-blockchain-api). [Download Postman collection](https://hirosystems.github.io/stacks-blockchain-api/collection.json). - version: 9.0.0-next.34 + version: 9.0.0-next.35 components: schemas: {} paths: @@ -91939,9 +91939,15 @@ paths: type: string memo: anyOf: - - description: Hex encoded arbitrary message, up to 34 bytes length (should try - decoding to an ASCII string) - type: string + - type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string - type: "null" - title: SmartContractTransactionSummary description: Smart contract transaction summary @@ -92771,9 +92777,15 @@ paths: type: string memo: anyOf: - - description: Hex encoded arbitrary message, up to 34 bytes length (should try - decoding to an ASCII string) - type: string + - type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string - type: "null" - title: SmartContractTransactionSummary description: Smart contract transaction summary @@ -97817,6 +97829,627 @@ paths: type: string message: type: string + /extended/v3/transactions/{tx_id}/events: + get: + operationId: get_transaction_events + summary: Get transaction events + tags: + - Transactions + description: Retrieves events for a given transaction ID + parameters: + - schema: + minimum: 1 + default: 20 + maximum: 100 + type: integer + in: query + name: limit + required: false + description: Number of results per page + - schema: + pattern: ^[0-9]+$ + type: string + in: query + name: cursor + required: false + description: "Cursor for paginating transaction events. Format: event_index" + - schema: + pattern: ^(0x)?[a-fA-F0-9]{64}$ + title: Transaction ID + type: string + example: "0xf6bd5f4a7b26184a3466340b2e99fd003b4962c0e382a7e4b6a13df3dd7a91c6" + in: path + name: tx_id + required: true + description: Transaction ID + responses: + "200": + description: Default Response + content: + application/json: + schema: + type: object + required: + - total + - limit + - cursor + - results + properties: + total: + type: integer + example: 1 + limit: + minimum: 1 + default: 20 + maximum: 100 + description: Number of results per page + type: integer + cursor: + type: object + required: + - next + - previous + - current + properties: + next: + anyOf: + - pattern: ^[0-9]+$ + description: "Cursor for paginating transaction events. Format: event_index" + type: string + - type: "null" + previous: + anyOf: + - pattern: ^[0-9]+$ + description: "Cursor for paginating transaction events. Format: event_index" + type: string + - type: "null" + current: + anyOf: + - pattern: ^[0-9]+$ + description: "Cursor for paginating transaction events. Format: event_index" + type: string + - type: "null" + results: + type: array + items: + anyOf: + - type: object + required: + - event_index + - type + - contract_log + properties: + event_index: + type: integer + type: + type: string + enum: + - contract_log + contract_log: + type: object + required: + - contract_id + - topic + - value + properties: + contract_id: + pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + topic: + type: string + enum: + - print + value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + - type: object + required: + - event_index + - type + - stx_lock + properties: + event_index: + type: integer + type: + type: string + enum: + - stx_lock + stx_lock: + type: object + required: + - amount + - unlock_bitcoin_height + - address + properties: + amount: + pattern: ^[0-9]+$ + title: Amount + description: Amount + examples: + - "1000000" + type: string + unlock_bitcoin_height: + title: Block height + description: Block height + examples: + - 777678 + type: integer + address: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + - type: object + required: + - event_index + - type + - stx_asset + properties: + event_index: + type: integer + type: + type: string + enum: + - stx_asset + stx_asset: + anyOf: + - type: object + required: + - type + - sender + - recipient + - amount + - memo + properties: + type: + type: string + enum: + - transfer + sender: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + recipient: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + amount: + pattern: ^[0-9]+$ + title: Amount + description: Amount + examples: + - "1000000" + type: string + memo: + anyOf: + - type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + - type: "null" + - type: object + required: + - type + - recipient + - amount + properties: + type: + type: string + enum: + - mint + recipient: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + amount: + pattern: ^[0-9]+$ + title: Amount + description: Amount + examples: + - "1000000" + type: string + - type: object + required: + - type + - sender + - amount + properties: + type: + type: string + enum: + - burn + sender: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + amount: + pattern: ^[0-9]+$ + title: Amount + description: Amount + examples: + - "1000000" + type: string + - type: object + required: + - event_index + - type + - ft_asset + properties: + event_index: + type: integer + type: + type: string + enum: + - ft_asset + ft_asset: + anyOf: + - type: object + required: + - type + - asset_identifier + - sender + - recipient + - amount + properties: + type: + type: string + enum: + - transfer + asset_identifier: + pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}::[a-zA-Z-]+$ + title: Asset Identifier + description: Asset Identifier + examples: + - SP000000000000000000002Q6VF78.pox-3::stx-token + type: string + sender: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + recipient: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + amount: + pattern: ^[0-9]+$ + title: Amount + description: Amount + examples: + - "1000000" + type: string + - type: object + required: + - type + - recipient + - asset_identifier + - amount + properties: + type: + type: string + enum: + - mint + recipient: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + asset_identifier: + pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}::[a-zA-Z-]+$ + title: Asset Identifier + description: Asset Identifier + examples: + - SP000000000000000000002Q6VF78.pox-3::stx-token + type: string + amount: + pattern: ^[0-9]+$ + title: Amount + description: Amount + examples: + - "1000000" + type: string + - type: object + required: + - type + - sender + - asset_identifier + - amount + properties: + type: + type: string + enum: + - burn + sender: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + asset_identifier: + pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}::[a-zA-Z-]+$ + title: Asset Identifier + description: Asset Identifier + examples: + - SP000000000000000000002Q6VF78.pox-3::stx-token + type: string + amount: + pattern: ^[0-9]+$ + title: Amount + description: Amount + examples: + - "1000000" + type: string + - type: object + required: + - event_index + - type + - nft_asset + properties: + event_index: + type: integer + type: + type: string + enum: + - nft_asset + nft_asset: + anyOf: + - type: object + required: + - type + - asset_identifier + - sender + - recipient + - value + properties: + type: + type: string + enum: + - transfer + asset_identifier: + pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}::[a-zA-Z-]+$ + title: Asset Identifier + description: Asset Identifier + examples: + - SP000000000000000000002Q6VF78.pox-3::stx-token + type: string + sender: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + recipient: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + - type: object + required: + - type + - recipient + - asset_identifier + - value + properties: + type: + type: string + enum: + - mint + recipient: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + asset_identifier: + pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}::[a-zA-Z-]+$ + title: Asset Identifier + description: Asset Identifier + examples: + - SP000000000000000000002Q6VF78.pox-3::stx-token + type: string + value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + - type: object + required: + - type + - sender + - asset_identifier + - value + properties: + type: + type: string + enum: + - burn + sender: + anyOf: + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41} + title: Stacks Address + description: Stacks Address + examples: + - SP318Q55DEKHRXJK696033DQN5C54D9K2EE6DHRWP + type: string + - pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$ + title: Smart Contract ID + description: Smart Contract ID + examples: + - SP000000000000000000002Q6VF78.pox-3 + type: string + asset_identifier: + pattern: ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}::[a-zA-Z-]+$ + title: Asset Identifier + description: Asset Identifier + examples: + - SP000000000000000000002Q6VF78.pox-3::stx-token + type: string + value: + type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string + 4XX: + description: Default Response + content: + application/json: + schema: + title: Error Response + additionalProperties: true + type: object + required: + - error + properties: + error: + type: string + message: + type: string /extended/v3/mempool/transactions: get: operationId: get_mempool_transactions @@ -98635,9 +99268,15 @@ paths: type: string memo: anyOf: - - description: Hex encoded arbitrary message, up to 34 bytes length (should try - decoding to an ASCII string) - type: string + - type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string - type: "null" - title: SmartContractTransactionSummary description: Smart contract transaction summary diff --git a/package-lock.json b/package-lock.json index e5446676b..fe784dedf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.34", + "version": "9.0.0-next.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.34", + "version": "9.0.0-next.35", "license": "GPL-3.0", "dependencies": { "@fastify/cors": "11.2.0", diff --git a/package.json b/package.json index e02d24542..92e69ee99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.34", + "version": "9.0.0-next.35", "type": "module", "main": "./lib/index.js", "scripts": { From 32d61afd93f1437a237b59a0179edd301c1385b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20C=C3=A1rdenas?= <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 27 May 2026 21:06:33 -0600 Subject: [PATCH 12/31] fix: decode memo as straight ascii (#2568) --- src/api/schemas/v3/entities/common.ts | 6 ++++++ .../v3/entities/mempool-transaction-summaries.ts | 8 ++------ .../schemas/v3/entities/mempool-transactions.ts | 8 ++------ src/api/schemas/v3/entities/transactions.ts | 12 ++++++------ src/api/serializers/v3/mempool-transactions.ts | 16 +++++++++++++--- src/api/serializers/v3/transaction-events.ts | 4 ++-- src/api/serializers/v3/transactions.ts | 10 ++++++++-- tests/api/v3/blocks.test.ts | 2 +- tests/api/v3/mempool.test.ts | 5 ++++- tests/api/v3/principals.test.ts | 2 +- tests/api/v3/transactions.test.ts | 2 +- 11 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/api/schemas/v3/entities/common.ts b/src/api/schemas/v3/entities/common.ts index 16356e6fe..64ca5ac07 100644 --- a/src/api/schemas/v3/entities/common.ts +++ b/src/api/schemas/v3/entities/common.ts @@ -66,6 +66,12 @@ export const DecodedClarityValueSchema = Type.Object({ }); export type DecodedClarityValue = Static; +export const DecodedStxTransferMemoSchema = Type.Object({ + hex: Type.String(), + repr: Type.String(), +}); +export type DecodedStxTransferMemo = Static; + export const ExecutionCostSchema = Type.Object({ read_count: Type.Integer({ description: 'Number of reads in the transaction', diff --git a/src/api/schemas/v3/entities/mempool-transaction-summaries.ts b/src/api/schemas/v3/entities/mempool-transaction-summaries.ts index 11cbe11fc..726c80cda 100644 --- a/src/api/schemas/v3/entities/mempool-transaction-summaries.ts +++ b/src/api/schemas/v3/entities/mempool-transaction-summaries.ts @@ -1,6 +1,7 @@ import { Static, Type } from '@sinclair/typebox'; import { TransactionSenderSchema } from './transaction-summaries.js'; import { Nullable } from '../../v1/util.js'; +import { DecodedStxTransferMemoSchema } from './common.js'; const MempoolTransactionStatusSchema = Type.Union( [ @@ -45,12 +46,7 @@ export const TokenTransferMempoolTransactionSummarySchema = Type.Composite( amount: Type.String({ description: 'Transfer amount as Integer string (64-bit unsigned integer)', }), - memo: Nullable( - Type.String({ - description: - 'Hex encoded arbitrary message, up to 34 bytes length (should try decoding to an ASCII string)', - }) - ), + memo: Nullable(DecodedStxTransferMemoSchema), }), }), ], diff --git a/src/api/schemas/v3/entities/mempool-transactions.ts b/src/api/schemas/v3/entities/mempool-transactions.ts index 8ed6e9ded..a2497b27b 100644 --- a/src/api/schemas/v3/entities/mempool-transactions.ts +++ b/src/api/schemas/v3/entities/mempool-transactions.ts @@ -2,7 +2,7 @@ import { Static, Type } from '@sinclair/typebox'; import { BaseMempoolTransactionSummarySchema } from './mempool-transaction-summaries.js'; import { PostConditionSchema } from './post-conditions.js'; import { Nullable } from '../../v1/util.js'; -import { DecodedClarityValueSchema } from './common.js'; +import { DecodedClarityValueSchema, DecodedStxTransferMemoSchema } from './common.js'; const BaseMempoolTransactionSchema = Type.Composite([ BaseMempoolTransactionSummarySchema, @@ -32,11 +32,7 @@ const TokenTransferMempoolTransactionSchema = Type.Composite([ amount: Type.String({ description: 'Amount of the token transfer', }), - memo: Nullable( - Type.String({ - description: 'Memo of the token transfer', - }) - ), + memo: Nullable(DecodedStxTransferMemoSchema), }), }), ]); diff --git a/src/api/schemas/v3/entities/transactions.ts b/src/api/schemas/v3/entities/transactions.ts index 71f3603ea..26bf4797b 100644 --- a/src/api/schemas/v3/entities/transactions.ts +++ b/src/api/schemas/v3/entities/transactions.ts @@ -2,7 +2,11 @@ import { Static, Type } from '@sinclair/typebox'; import { BaseTransactionSummarySchema, TenureChangeCauseSchema } from './transaction-summaries.js'; import { PostConditionSchema } from './post-conditions.js'; import { Nullable } from '../../v1/util.js'; -import { DecodedClarityValueSchema, ExecutionCostSchema } from './common.js'; +import { + DecodedClarityValueSchema, + DecodedStxTransferMemoSchema, + ExecutionCostSchema, +} from './common.js'; const BaseTransactionSchema = Type.Composite([ BaseTransactionSummarySchema, @@ -45,11 +49,7 @@ const TokenTransferTransactionSchema = Type.Composite([ amount: Type.String({ description: 'Amount of the token transfer', }), - memo: Nullable( - Type.String({ - description: 'Memo of the token transfer', - }) - ), + memo: Nullable(DecodedStxTransferMemoSchema), }), }), ]); diff --git a/src/api/serializers/v3/mempool-transactions.ts b/src/api/serializers/v3/mempool-transactions.ts index b7a9f58c0..f4410dd37 100644 --- a/src/api/serializers/v3/mempool-transactions.ts +++ b/src/api/serializers/v3/mempool-transactions.ts @@ -23,7 +23,7 @@ import { } from '../../schemas/v3/entities/mempool-transactions.js'; import { TransactionIncludeField } from '../../schemas/v3/entities/transactions.js'; import { serializePostCondition } from './post-conditions.js'; -import { decodeClarityValueList, decodePostConditions } from '@stacks/codec'; +import { decodeClarityValueList, decodePostConditions, memoToString } from '@stacks/codec'; /** * Parses a database mempool transaction summary status into a mempool transaction summary status. @@ -83,7 +83,12 @@ export function serializeDbMempoolTransactionSummary( token_transfer: { recipient: summary.token_transfer_recipient_address!, amount: summary.token_transfer_amount!, - memo: summary.token_transfer_memo, + memo: summary.token_transfer_memo + ? { + hex: summary.token_transfer_memo, + repr: memoToString(summary.token_transfer_memo), + } + : null, }, }; return tokenTransfer; @@ -166,7 +171,12 @@ export function serializeDbMempoolTransaction( token_transfer: { recipient: transaction.token_transfer_recipient_address!, amount: transaction.token_transfer_amount!, - memo: transaction.token_transfer_memo, + memo: transaction.token_transfer_memo + ? { + hex: transaction.token_transfer_memo, + repr: memoToString(transaction.token_transfer_memo), + } + : null, }, }; return tokenTransfer; diff --git a/src/api/serializers/v3/transaction-events.ts b/src/api/serializers/v3/transaction-events.ts index b331d454b..d2d1755e3 100644 --- a/src/api/serializers/v3/transaction-events.ts +++ b/src/api/serializers/v3/transaction-events.ts @@ -1,7 +1,7 @@ import { TransactionEvent } from '../../schemas/v3/entities/transaction-events.js'; import { DbTransactionEvent } from '../../../datastore/v3/types.js'; import { DbAssetEventTypeId, DbEventTypeId } from '../../../datastore/common.js'; -import { decodeClarityValueToRepr } from '@stacks/codec'; +import { decodeClarityValueToRepr, memoToString } from '@stacks/codec'; /** * Serializes a database transaction event into a transaction event. @@ -38,7 +38,7 @@ export function serializeDbTransactionEvent(event: DbTransactionEvent): Transact memo: event.memo ? { hex: event.memo, - repr: decodeClarityValueToRepr(event.memo), + repr: memoToString(event.memo), } : null, }, diff --git a/src/api/serializers/v3/transactions.ts b/src/api/serializers/v3/transactions.ts index d7b46a4bd..0f473a1fe 100644 --- a/src/api/serializers/v3/transactions.ts +++ b/src/api/serializers/v3/transactions.ts @@ -34,6 +34,7 @@ import { decodeClarityValueList, decodeClarityValueToRepr, decodePostConditions, + memoToString, } from '@stacks/codec'; import { serializePostCondition } from './post-conditions.js'; import { serializeDbMempoolTransaction } from './mempool-transactions.js'; @@ -126,7 +127,7 @@ export function serializeDbTransactionSummary(summary: DbTransactionSummary): Tr memo: summary.token_transfer_memo ? { hex: summary.token_transfer_memo, - repr: decodeClarityValueToRepr(summary.token_transfer_memo), + repr: memoToString(summary.token_transfer_memo), } : null, }, @@ -263,7 +264,12 @@ export function serializeDbTransaction( token_transfer: { recipient: transaction.token_transfer_recipient_address!, amount: transaction.token_transfer_amount!, - memo: transaction.token_transfer_memo, + memo: transaction.token_transfer_memo + ? { + hex: transaction.token_transfer_memo, + repr: memoToString(transaction.token_transfer_memo), + } + : null, }, }; return tokenTransfer; diff --git a/tests/api/v3/blocks.test.ts b/tests/api/v3/blocks.test.ts index 39cd0faae..f795a4819 100644 --- a/tests/api/v3/blocks.test.ts +++ b/tests/api/v3/blocks.test.ts @@ -169,7 +169,7 @@ describe('blocks', () => { amount: '100', memo: { hex: '0x0d0000000568656c6c6f', - repr: '"hello"', + repr: 'hello', }, }, }); diff --git a/tests/api/v3/mempool.test.ts b/tests/api/v3/mempool.test.ts index 13df00b14..ec469fbac 100644 --- a/tests/api/v3/mempool.test.ts +++ b/tests/api/v3/mempool.test.ts @@ -133,7 +133,10 @@ describe('mempool', () => { token_transfer: { recipient: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6', amount: '500', - memo: '0x0d0000000568656c6c6f', + memo: { + hex: '0x0d0000000568656c6c6f', + repr: 'hello', + }, }, }); }); diff --git a/tests/api/v3/principals.test.ts b/tests/api/v3/principals.test.ts index 18c588c49..93d3ebcda 100644 --- a/tests/api/v3/principals.test.ts +++ b/tests/api/v3/principals.test.ts @@ -181,7 +181,7 @@ describe('principals', () => { amount: '100', memo: { hex: '0x0d0000000568656c6c6f', - repr: '"hello"', + repr: 'hello', }, }, }, diff --git a/tests/api/v3/transactions.test.ts b/tests/api/v3/transactions.test.ts index 4f16724a3..400fbdf4d 100644 --- a/tests/api/v3/transactions.test.ts +++ b/tests/api/v3/transactions.test.ts @@ -134,7 +134,7 @@ describe('transactions', () => { amount: '100', memo: { hex: '0x0d0000000568656c6c6f', - repr: '"hello"', + repr: 'hello', }, }, }); From 2b74100a5f8b8d4bfe07fd00de0622773f504c37 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 27 May 2026 21:07:26 -0600 Subject: [PATCH 13/31] chore(release): v9.0.0-next.36 [skip ci] --- client/package-lock.json | 4 ++-- client/package.json | 2 +- client/src/generated/schema.d.ts | 15 ++++++++++--- openapi.yaml | 36 +++++++++++++++++++++++++------- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index b5ef68e3f..9d2f9e51c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.35", + "version": "9.0.0-next.36", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.35", + "version": "9.0.0-next.36", "license": "GPL-3.0", "dependencies": { "@types/node": "20.14.14", diff --git a/client/package.json b/client/package.json index 47029f516..843d0316a 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@stacks/blockchain-api-client", - "version": "9.0.0-next.35", + "version": "9.0.0-next.36", "access": "public", "description": "Client for the Stacks Blockchain API", "homepage": "https://github.com/hirosystems/stacks-blockchain-api/tree/master/client#readme", diff --git a/client/src/generated/schema.d.ts b/client/src/generated/schema.d.ts index dc6072fff..59af5ea53 100644 --- a/client/src/generated/schema.d.ts +++ b/client/src/generated/schema.d.ts @@ -32598,7 +32598,10 @@ export interface operations { recipient: string; /** @description Amount of the token transfer */ amount: string; - memo: string | null; + memo: { + hex: string; + repr: string; + } | null; }; } | { /** @description Transaction ID */ @@ -33405,7 +33408,10 @@ export interface operations { recipient: string; /** @description Amount of the token transfer */ amount: string; - memo: string | null; + memo: { + hex: string; + repr: string; + } | null; }; } | { /** @description Transaction ID */ @@ -34204,7 +34210,10 @@ export interface operations { recipient: string; /** @description Transfer amount as Integer string (64-bit unsigned integer) */ amount: string; - memo: string | null; + memo: { + hex: string; + repr: string; + } | null; }; } | { /** @description Transaction ID */ diff --git a/openapi.yaml b/openapi.yaml index 02db6b05d..21bd44d67 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4,7 +4,7 @@ info: description: Welcome to the API reference overview for the [Stacks Blockchain API](https://docs.hiro.so/stacks-blockchain-api). [Download Postman collection](https://hirosystems.github.io/stacks-blockchain-api/collection.json). - version: 9.0.0-next.35 + version: 9.0.0-next.36 components: schemas: {} paths: @@ -93821,8 +93821,15 @@ paths: type: string memo: anyOf: - - description: Memo of the token transfer - type: string + - type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string - type: "null" - type: object required: @@ -96204,8 +96211,15 @@ paths: type: string memo: anyOf: - - description: Memo of the token transfer - type: string + - type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string - type: "null" - type: object required: @@ -98621,9 +98635,15 @@ paths: type: string memo: anyOf: - - description: Hex encoded arbitrary message, up to 34 bytes length (should try - decoding to an ASCII string) - type: string + - type: object + required: + - hex + - repr + properties: + hex: + type: string + repr: + type: string - type: "null" - title: SmartContractMempoolTransactionSummary description: Smart contract mempool transaction summary diff --git a/package-lock.json b/package-lock.json index fe784dedf..3332306ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.35", + "version": "9.0.0-next.36", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.35", + "version": "9.0.0-next.36", "license": "GPL-3.0", "dependencies": { "@fastify/cors": "11.2.0", diff --git a/package.json b/package.json index 92e69ee99..646818ccd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hirosystems/stacks-blockchain-api", - "version": "9.0.0-next.35", + "version": "9.0.0-next.36", "type": "module", "main": "./lib/index.js", "scripts": { From 32ef39bdbc390f8fce9bc5305f76c0af19d26063 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Fri, 29 May 2026 10:52:21 -0600 Subject: [PATCH 14/31] start consuming bond events --- migrations/1779487960678_bonds.ts | 8 +- .../1779487971367_bond-allowlist-entries.ts | 10 +- .../1779487975550_bond-registrations.ts | 21 ++- src/datastore/common.ts | 51 +++++-- src/datastore/pg-write-store.ts | 134 +++++++++++++++--- src/event-stream/event-server.ts | 13 +- 6 files changed, 184 insertions(+), 53 deletions(-) diff --git a/migrations/1779487960678_bonds.ts b/migrations/1779487960678_bonds.ts index 3cbd2ca5c..5d7b9b7ba 100644 --- a/migrations/1779487960678_bonds.ts +++ b/migrations/1779487960678_bonds.ts @@ -3,7 +3,7 @@ import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; export const up = (pgm: MigrationBuilder) => { - pgm.createTable('pox5_bonds', { + pgm.createTable('bonds', { id: { type: 'bigserial', primaryKey: true, @@ -70,7 +70,7 @@ export const up = (pgm: MigrationBuilder) => { } }); pgm.createIndex( - 'pox5_bonds', + 'bonds', [ { name: 'block_height', sort: 'DESC' }, { name: 'microblock_sequence', sort: 'DESC' }, @@ -81,11 +81,11 @@ export const up = (pgm: MigrationBuilder) => { where: 'canonical = TRUE AND microblock_canonical = TRUE', } ); - pgm.createIndex('pox5_bonds', 'bond_index', { + pgm.createIndex('bonds', 'bond_index', { where: 'canonical = TRUE AND microblock_canonical = TRUE', }); }; export const down = (pgm: MigrationBuilder) => { - pgm.dropTable('pox5_bonds'); + pgm.dropTable('bonds'); }; diff --git a/migrations/1779487971367_bond-allowlist-entries.ts b/migrations/1779487971367_bond-allowlist-entries.ts index 9b53471b5..02b502e62 100644 --- a/migrations/1779487971367_bond-allowlist-entries.ts +++ b/migrations/1779487971367_bond-allowlist-entries.ts @@ -3,7 +3,7 @@ import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; export const up = (pgm: MigrationBuilder) => { - pgm.createTable('pox5_bond_allowlist_entries', { + pgm.createTable('bond_allowlist_entries', { id: { type: 'bigserial', primaryKey: true, @@ -59,7 +59,7 @@ export const up = (pgm: MigrationBuilder) => { }); pgm.createIndex( - 'pox5_bond_allowlist_entries', + 'bond_allowlist_entries', [ { name: 'block_height', sort: 'DESC' }, { name: 'microblock_sequence', sort: 'DESC' }, @@ -70,14 +70,14 @@ export const up = (pgm: MigrationBuilder) => { where: 'canonical = TRUE AND microblock_canonical = TRUE', } ); - pgm.createIndex('pox5_bond_allowlist_entries', 'bond_index', { + pgm.createIndex('bond_allowlist_entries', 'bond_index', { where: 'canonical = TRUE AND microblock_canonical = TRUE', }); - pgm.createIndex('pox5_bond_allowlist_entries', 'staker', { + pgm.createIndex('bond_allowlist_entries', 'staker', { where: 'canonical = TRUE AND microblock_canonical = TRUE', }); }; export const down = (pgm: MigrationBuilder) => { - pgm.dropTable('pox5_bond_allowlist_entries'); + pgm.dropTable('bond_allowlist_entries'); }; diff --git a/migrations/1779487975550_bond-registrations.ts b/migrations/1779487975550_bond-registrations.ts index 51b784a82..fcf60d1db 100644 --- a/migrations/1779487975550_bond-registrations.ts +++ b/migrations/1779487975550_bond-registrations.ts @@ -3,7 +3,7 @@ import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; export const up = (pgm: MigrationBuilder) => { - pgm.createTable('pox5_bond_registrations', { + pgm.createTable('bond_registrations', { id: { type: 'bigserial', primaryKey: true, @@ -82,29 +82,24 @@ export const up = (pgm: MigrationBuilder) => { }, }); - pgm.createIndex('pox5_bond_registrations', 'bond_index', { - where: 'canonical = TRUE AND microblock_canonical = TRUE', - }); - pgm.createIndex('pox5_bond_registrations', 'pox_address', { - where: 'canonical = TRUE AND microblock_canonical = TRUE', - }); - pgm.createIndex('pox5_bond_registrations', 'signer_manager', { - where: 'canonical = TRUE AND microblock_canonical = TRUE', - }); pgm.createIndex( - 'pox5_bond_registrations', + 'bond_registrations', [ + 'bond_index', { name: 'block_height', sort: 'DESC' }, { name: 'microblock_sequence', sort: 'DESC' }, { name: 'tx_index', sort: 'DESC' }, - { name: 'event_index', sort: 'DESC' }, + { name: 'id', sort: 'DESC' }, ], { where: 'canonical = TRUE AND microblock_canonical = TRUE', } ); + pgm.createIndex('bond_registrations', ['bond_index', 'staker'], { + where: 'canonical = TRUE AND microblock_canonical = TRUE', + }); }; export const down = (pgm: MigrationBuilder) => { - pgm.dropTable('pox5_bond_registrations'); + pgm.dropTable('bond_registrations'); }; diff --git a/src/datastore/common.ts b/src/datastore/common.ts index b304f58d2..40826bd95 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1369,17 +1369,9 @@ export interface Pox4SyntheticEventInsertValues { start_cycle_id?: PgNumeric | null; } -export interface Pox5SyntheticEventInsertValues { +export interface Pox5SyntheticEventInsertValues extends DbTxLocation { event_index: number; - tx_id: PgBytea; - tx_index: number; - block_height: number; - index_block_hash: PgBytea; - parent_index_block_hash: PgBytea; - microblock_hash: PgBytea; - microblock_sequence: number; - microblock_canonical: boolean; - canonical: boolean; + name: string; data: PgJsonb; } @@ -1633,3 +1625,42 @@ export interface DbSmartContractStatus { status: DbTxStatus; block_height?: number; } + +export interface DbTxLocation { + tx_id: string; + tx_index: number; + block_height: number; + index_block_hash: string; + parent_index_block_hash: string; + microblock_hash: string; + microblock_sequence: number; + microblock_canonical: boolean; + canonical: boolean; +} + +export interface DbBondInsertValues extends DbTxLocation { + bond_index: number; + target_rate: number; + stx_value_ratio: number; + min_ustx_ratio: number; + early_unlock_signers: string; + early_unlock_admin: string; +} + +export interface DbBondRegistrationInsertValues extends DbTxLocation { + bond_index: number; + signer: string; + staker: string; + amount_ustx: string; + sats_total: string; + first_reward_cycle: number; + unlock_burn_height: number; + unlock_cycle: number; + is_l1_lock: boolean; +} + +export interface DbBondAllowlistEntryInsertValues extends DbTxLocation { + bond_index: number; + staker: string; + max_sats: string; +} diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 60d3cb7d1..1da5f63b0 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -65,6 +65,10 @@ import { DbAssetEventTypeId, DbBurnBlockPoxTx, Pox5SyntheticEventInsertValues, + DbBondInsertValues, + DbTxLocation, + DbBondRegistrationInsertValues, + DbBondAllowlistEntryInsertValues, } from './common.js'; import { BLOCK_COLUMNS, @@ -98,7 +102,14 @@ import { PgServer, getConnectionArgs, getConnectionConfig } from './connection.j import { BigNumber } from 'bignumber.js'; import { RedisNotifier } from './redis-notifier.js'; import { ENV } from '../env.js'; -import { Pox4EventName } from '@stacks/codec'; +import { + Pox4EventName, + Pox5EventAddToAllowlist, + Pox5EventName, + Pox5EventRegisterForBond, + Pox5EventSetupBond, + Pox5EventUpdateBondRegistration, +} from '@stacks/codec'; const INSERT_BATCH_SIZE = 500; @@ -504,32 +515,119 @@ export class PgWriteStore extends PgStore { } private async insertPox5SyntheticEvents(sql: PgSqlClient, txs: DataStoreTxEventData[]) { - const values: Pox5SyntheticEventInsertValues[] = []; + const poxValues: Pox5SyntheticEventInsertValues[] = []; for (const tx of txs) { if (tx.pox5Events.length === 0) continue; - values.push( - ...tx.pox5Events.map(e => ({ - event_index: e.event_index, - tx_id: e.tx_id, - tx_index: e.tx_index, - block_height: e.block_height, - index_block_hash: tx.tx.index_block_hash, - parent_index_block_hash: tx.tx.parent_index_block_hash, - microblock_hash: tx.tx.microblock_hash, - microblock_sequence: tx.tx.microblock_sequence, - microblock_canonical: tx.tx.microblock_canonical, - canonical: e.canonical, - data: e.data, - })) - ); + const txLocation: DbTxLocation = { + tx_id: tx.tx.tx_id, + tx_index: tx.tx.tx_index, + block_height: tx.tx.block_height, + index_block_hash: tx.tx.index_block_hash, + parent_index_block_hash: tx.tx.parent_index_block_hash, + microblock_hash: tx.tx.microblock_hash, + microblock_sequence: tx.tx.microblock_sequence, + microblock_canonical: tx.tx.microblock_canonical, + canonical: tx.tx.canonical, + }; + for (const poxEvent of tx.pox5Events) { + poxValues.push({ + ...txLocation, + event_index: poxEvent.event_index, + name: poxEvent.name, + data: poxEvent.data, + }); + switch (poxEvent.name) { + case Pox5EventName.SetupBond: + await this.updateBond(sql, txLocation, poxEvent); + break; + case Pox5EventName.RegisterForBond: + case Pox5EventName.UpdateBondRegistration: + await this.updateBondRegistration(sql, txLocation, poxEvent); + break; + case Pox5EventName.AddToAllowlist: + await this.updateBondAllowlistEntry(sql, txLocation, poxEvent); + break; + } + } } - for (const batch of batchIterate(values, INSERT_BATCH_SIZE)) { + for (const batch of batchIterate(poxValues, INSERT_BATCH_SIZE)) { await sql` INSERT INTO pox5_events ${sql(batch)} `; } } + private async updateBond(sql: PgSqlClient, txLocation: DbTxLocation, event: Pox5EventSetupBond) { + const bond: DbBondInsertValues = { + ...txLocation, + bond_index: parseInt(event.data.bond_index), + target_rate: parseInt(event.data.target_rate), + stx_value_ratio: parseInt(event.data.stx_value_ratio), + min_ustx_ratio: parseInt(event.data.min_ustx_ratio), + early_unlock_signers: event.data.early_unlock_signers, + early_unlock_admin: event.data.early_unlock_admin, + }; + await sql` + INSERT INTO bonds ${sql(bond)} + `; + } + + private async updateBondRegistration( + sql: PgSqlClient, + txLocation: DbTxLocation, + event: Pox5EventRegisterForBond | Pox5EventUpdateBondRegistration + ) { + if (event.name === Pox5EventName.RegisterForBond) { + const bondRegistration: DbBondRegistrationInsertValues = { + ...txLocation, + bond_index: parseInt(event.data.bond_index), + signer: event.data.signer, + staker: event.data.staker, + amount_ustx: event.data.amount_ustx, + sats_total: event.data.sats_total, + first_reward_cycle: parseInt(event.data.first_reward_cycle), + unlock_burn_height: parseInt(event.data.unlock_burn_height), + unlock_cycle: parseInt(event.data.unlock_cycle), + is_l1_lock: event.data.is_l1_lock, + }; + await sql` + INSERT INTO bond_registrations ${sql(bondRegistration)} + `; + } else { + const updateResult = await sql` + UPDATE bond_registrations SET + signer = ${event.data.signer}, + sats_total = ${event.data.amount_sats}, + amount_ustx = ${event.data.amount_ustx} + WHERE bond_index = ${parseInt(event.data.bond_index)} + AND staker = ${event.data.staker} + AND canonical = true + AND microblock_canonical = true + `; + if (updateResult.count === 0) { + logger.warn( + `Bond registration not found for bond index ${event.data.bond_index} and staker ${event.data.staker}` + ); + } + } + } + + private async updateBondAllowlistEntry( + sql: PgSqlClient, + txLocation: DbTxLocation, + event: Pox5EventAddToAllowlist + ) { + const bondAllowlistEntry: DbBondAllowlistEntryInsertValues = { + ...txLocation, + bond_index: parseInt(event.data.bond_index), + staker: event.data.staker, + max_sats: event.data.max_sats, + }; + await sql` + INSERT INTO bond_allowlist_entries ${sql(bondAllowlistEntry)} + `; + } + private async updatePoxStateUnlockHeight(sql: PgSqlClient, data: DataStoreBlockUpdateData) { if (data.pox_v1_unlock_height !== undefined) { // update the pox_state.pox_v1_unlock_height singleton diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index 7312a936b..2698afc03 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -26,6 +26,7 @@ import { DbTxStatus, DbPoxSetSigners, DbBurnBlockPoxTx, + DbPox5SyntheticEvent, } from '../datastore/common.js'; import { getTxSenderAddress, @@ -318,7 +319,8 @@ function parseDataStoreTxEventData( return dbTx; }); - const poxEventLogs: Map = new Map(); + const poxEventLogs: Map = + new Map(); for (const event of events) { if (!event.committed) { @@ -398,7 +400,12 @@ function parseDataStoreTxEventData( break; } case 'pox5': { - // dbTx.pox5Events.push(dbPoxEvent); + const dbPoxEvent: DbPox5SyntheticEvent = { + ...dbEvent, + ...poxEvent, + }; + dbTx.pox5Events.push(dbPoxEvent); + poxEventLogs.set(dbPoxEvent, entry); break; } } @@ -569,7 +576,7 @@ 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()) { + for (const poxEvent of [tx.pox2Events, tx.pox3Events, tx.pox4Events, tx.pox5Events].flat()) { const associatedLogEvent = poxEventLogs.get(poxEvent); if (!associatedLogEvent) { throw new Error(`Missing associated contract log event for pox event ${poxEvent.tx_id}`); From 67739ff6a7cba841f32e12fdcdf5a9eaaae55fdc Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Fri, 29 May 2026 14:51:30 -0600 Subject: [PATCH 15/31] add some position updates --- .../1779742831642_principal-bond-positions.ts | 4 + src/datastore/common.ts | 17 +++ src/datastore/pg-write-store.ts | 107 ++++++++++++++++-- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/migrations/1779742831642_principal-bond-positions.ts b/migrations/1779742831642_principal-bond-positions.ts index b07b3dd04..a4e1d5aba 100644 --- a/migrations/1779742831642_principal-bond-positions.ts +++ b/migrations/1779742831642_principal-bond-positions.ts @@ -73,6 +73,10 @@ export function up(pgm: MigrationBuilder): void { notNull: true, }, }); + + pgm.createIndex('principal_bond_positions', ['principal', 'bond_index'], { + unique: true, + }); } export function down(pgm: MigrationBuilder): void { diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 40826bd95..806265d7c 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1664,3 +1664,20 @@ export interface DbBondAllowlistEntryInsertValues extends DbTxLocation { staker: string; max_sats: string; } + +export enum DbPrincipalBondPositionStatus { + Enrolled = 0, + Running = 1, + Unlocked = 2, + EarlyExit = 3, +} + +export interface DbPrincipalBondPositionInsertValues extends DbTxLocation { + principal: string; + bond_index: number; + status: DbPrincipalBondPositionStatus; + active: boolean; + btc_locked: string; + stx_locked: string; + btc_paid_out: string; +} diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 1da5f63b0..0b4092194 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -69,6 +69,8 @@ import { DbTxLocation, DbBondRegistrationInsertValues, DbBondAllowlistEntryInsertValues, + DbPrincipalBondPositionInsertValues, + DbPrincipalBondPositionStatus, } from './common.js'; import { BLOCK_COLUMNS, @@ -105,9 +107,11 @@ import { ENV } from '../env.js'; import { Pox4EventName, Pox5EventAddToAllowlist, + Pox5EventAnnounceL1EarlyExit, Pox5EventName, Pox5EventRegisterForBond, Pox5EventSetupBond, + Pox5EventUnstakeSbtc, Pox5EventUpdateBondRegistration, } from '@stacks/codec'; @@ -540,12 +544,20 @@ export class PgWriteStore extends PgStore { case Pox5EventName.SetupBond: await this.updateBond(sql, txLocation, poxEvent); break; + case Pox5EventName.AddToAllowlist: + await this.updateBondAllowlistEntry(sql, txLocation, poxEvent); + break; case Pox5EventName.RegisterForBond: case Pox5EventName.UpdateBondRegistration: await this.updateBondRegistration(sql, txLocation, poxEvent); + await this.updatePrincipalBondPosition(sql, txLocation, poxEvent); break; - case Pox5EventName.AddToAllowlist: - await this.updateBondAllowlistEntry(sql, txLocation, poxEvent); + case Pox5EventName.AnnounceL1EarlyExit: + case Pox5EventName.UnstakeSbtc: + await this.updatePrincipalBondPosition(sql, txLocation, poxEvent); + break; + default: + logger.warn(`Unhandled pox-5 event: ${poxEvent.name}`); break; } } @@ -577,14 +589,20 @@ export class PgWriteStore extends PgStore { txLocation: DbTxLocation, event: Pox5EventRegisterForBond | Pox5EventUpdateBondRegistration ) { + const bondIndex = parseInt(event.data.bond_index); + const staker = event.data.staker; + const amountUstx = event.data.amount_ustx; + const satsTotal = + event.name === Pox5EventName.RegisterForBond ? event.data.sats_total : event.data.amount_sats; + if (event.name === Pox5EventName.RegisterForBond) { const bondRegistration: DbBondRegistrationInsertValues = { ...txLocation, - bond_index: parseInt(event.data.bond_index), + bond_index: bondIndex, signer: event.data.signer, - staker: event.data.staker, - amount_ustx: event.data.amount_ustx, - sats_total: event.data.sats_total, + staker, + amount_ustx: amountUstx, + sats_total: satsTotal, first_reward_cycle: parseInt(event.data.first_reward_cycle), unlock_burn_height: parseInt(event.data.unlock_burn_height), unlock_cycle: parseInt(event.data.unlock_cycle), @@ -597,10 +615,10 @@ export class PgWriteStore extends PgStore { const updateResult = await sql` UPDATE bond_registrations SET signer = ${event.data.signer}, - sats_total = ${event.data.amount_sats}, - amount_ustx = ${event.data.amount_ustx} - WHERE bond_index = ${parseInt(event.data.bond_index)} - AND staker = ${event.data.staker} + sats_total = ${satsTotal}, + amount_ustx = ${amountUstx} + WHERE bond_index = ${bondIndex} + AND staker = ${staker} AND canonical = true AND microblock_canonical = true `; @@ -612,6 +630,75 @@ export class PgWriteStore extends PgStore { } } + private async updatePrincipalBondPosition( + sql: PgSqlClient, + txLocation: DbTxLocation, + event: + | Pox5EventRegisterForBond + | Pox5EventUpdateBondRegistration + | Pox5EventAnnounceL1EarlyExit + | Pox5EventUnstakeSbtc + ) { + switch (event.name) { + case Pox5EventName.RegisterForBond: + case Pox5EventName.UpdateBondRegistration: { + const position: DbPrincipalBondPositionInsertValues = { + ...txLocation, + principal: event.data.staker, + bond_index: parseInt(event.data.bond_index), + status: DbPrincipalBondPositionStatus.Enrolled, + active: true, + btc_locked: + event.name === Pox5EventName.RegisterForBond + ? event.data.sats_total + : event.data.amount_sats, + stx_locked: event.data.amount_ustx, + btc_paid_out: '0', + }; + await sql` + INSERT INTO principal_bond_positions ${sql(position)} + ON CONFLICT (principal, bond_index) DO UPDATE SET + tx_id = EXCLUDED.tx_id, + tx_index = EXCLUDED.tx_index, + block_height = EXCLUDED.block_height, + index_block_hash = EXCLUDED.index_block_hash, + parent_index_block_hash = EXCLUDED.parent_index_block_hash, + microblock_hash = EXCLUDED.microblock_hash, + microblock_sequence = EXCLUDED.microblock_sequence, + microblock_canonical = EXCLUDED.microblock_canonical, + canonical = EXCLUDED.canonical, + status = EXCLUDED.status, + active = EXCLUDED.active, + btc_locked = EXCLUDED.btc_locked, + stx_locked = EXCLUDED.stx_locked + `; + break; + } + case Pox5EventName.AnnounceL1EarlyExit: + await sql` + UPDATE principal_bond_positions SET + status = ${DbPrincipalBondPositionStatus.EarlyExit}, + active = false + WHERE principal = ${event.data.staker} + AND bond_index = ${parseInt(event.data.bond_index)} + AND canonical = true + AND microblock_canonical = true + `; + return; + case Pox5EventName.UnstakeSbtc: + await sql` + UPDATE principal_bond_positions SET + ${event.data.new_amount_sats == '0' ? sql`status = ${DbPrincipalBondPositionStatus.EarlyExit}` : sql``} + btc_locked = ${event.data.new_amount_sats} + WHERE principal = ${event.data.staker} + AND bond_index = ${parseInt(event.data.bond_index)} + AND canonical = true + AND microblock_canonical = true + `; + return; + } + } + private async updateBondAllowlistEntry( sql: PgSqlClient, txLocation: DbTxLocation, From cf6dc4fc74c7534a1c077565adb9f76c8251239c Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Fri, 29 May 2026 17:24:26 -0600 Subject: [PATCH 16/31] bond rewards init --- ...1779745340752_bond-reward-distributions.ts | 77 ++++++++++++++++++- src/datastore/common.ts | 11 +++ src/datastore/pg-write-store.ts | 48 +++++++++++- 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/migrations/1779745340752_bond-reward-distributions.ts b/migrations/1779745340752_bond-reward-distributions.ts index 6ba1556f7..72a7b8631 100644 --- a/migrations/1779745340752_bond-reward-distributions.ts +++ b/migrations/1779745340752_bond-reward-distributions.ts @@ -2,6 +2,79 @@ import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; -export async function up(pgm: MigrationBuilder): Promise {} +export function up(pgm: MigrationBuilder): void { + pgm.createTable('bond_reward_distributions', { + bond_index: { + type: 'integer', + notNull: true, + }, + tx_id: { + type: 'bytea', + notNull: true, + }, + tx_index: { + type: 'smallint', + notNull: true, + }, + block_height: { + type: 'integer', + notNull: true, + }, + index_block_hash: { + type: 'bytea', + notNull: true, + }, + parent_index_block_hash: { + type: 'bytea', + notNull: true, + }, + microblock_hash: { + type: 'bytea', + notNull: true, + }, + microblock_sequence: { + type: 'integer', + notNull: true, + }, + microblock_canonical: { + type: 'boolean', + notNull: true, + }, + canonical: { + type: 'boolean', + notNull: true, + }, + remaining_rewards: { + type: 'numeric', + notNull: true, + }, + accrued_rewards: { + type: 'numeric', + notNull: true, + }, + new_reserve: { + type: 'numeric', + notNull: true, + }, + stx_staker_rewards: { + type: 'numeric', + notNull: true, + }, + stx_cycle: { + type: 'integer', + notNull: true, + }, + cycle_staked_ustx: { + type: 'numeric', + notNull: true, + }, + next_rewards_per_ustx: { + type: 'numeric', + notNull: true, + } + }); +} -export async function down(pgm: MigrationBuilder): Promise {} +export function down(pgm: MigrationBuilder): void { + pgm.dropTable('bond_reward_distributions'); +} diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 806265d7c..9f818dce1 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1681,3 +1681,14 @@ export interface DbPrincipalBondPositionInsertValues extends DbTxLocation { stx_locked: string; btc_paid_out: string; } + +export interface DbBondRewardDistributionInsertValues extends DbTxLocation { + bond_index: number; + remaining_rewards: string; + accrued_rewards: string; + new_reserve: string; + stx_staker_rewards: string; + stx_cycle: number; + cycle_staked_ustx: string; + next_rewards_per_ustx: string; +} diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 0b4092194..2a9bdd86e 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -71,6 +71,7 @@ import { DbBondAllowlistEntryInsertValues, DbPrincipalBondPositionInsertValues, DbPrincipalBondPositionStatus, + DbBondRewardDistributionInsertValues, } from './common.js'; import { BLOCK_COLUMNS, @@ -108,6 +109,7 @@ import { Pox4EventName, Pox5EventAddToAllowlist, Pox5EventAnnounceL1EarlyExit, + Pox5EventCalculateRewards, Pox5EventName, Pox5EventRegisterForBond, Pox5EventSetupBond, @@ -556,8 +558,23 @@ export class PgWriteStore extends PgStore { case Pox5EventName.UnstakeSbtc: await this.updatePrincipalBondPosition(sql, txLocation, poxEvent); break; - default: - logger.warn(`Unhandled pox-5 event: ${poxEvent.name}`); + case Pox5EventName.CalculateRewards: + await this.updateBondRewardDistribution(sql, txLocation, poxEvent); + break; + case Pox5EventName.BondDistribution: + // TODO: Implement + break; + case Pox5EventName.ClaimRewards: + break; + case Pox5EventName.Stake: + case Pox5EventName.StakeUpdate: + // TODO: Implement + break; + case Pox5EventName.Unstake: + // TODO: Implement + break; + case Pox5EventName.RegisterSigner: + // TODO: Implement break; } } @@ -699,6 +716,33 @@ export class PgWriteStore extends PgStore { } } + private async updateBondRewardDistribution( + sql: PgSqlClient, + txLocation: DbTxLocation, + event: Pox5EventCalculateRewards + ) { + const rewardDistributions: DbBondRewardDistributionInsertValues[] = []; + for (const bondIndex of event.data.bond_periods) { + // TODO: Divide rewards by bond period + rewardDistributions.push({ + ...txLocation, + bond_index: parseInt(bondIndex), + remaining_rewards: event.data.remaining_rewards, + accrued_rewards: event.data.accrued_rewards, + new_reserve: event.data.new_reserve, + stx_staker_rewards: event.data.stx_staker_rewards, + stx_cycle: parseInt(event.data.stx_cycle), + cycle_staked_ustx: event.data.cycle_staked_ustx, + next_rewards_per_ustx: event.data.next_rewards_per_ustx, + }); + } + for (const batch of batchIterate(rewardDistributions, INSERT_BATCH_SIZE)) { + await sql` + INSERT INTO bond_reward_distributions ${sql(batch)} + `; + } + } + private async updateBondAllowlistEntry( sql: PgSqlClient, txLocation: DbTxLocation, From 8574235d61949237ce8897721ae61914243c0e0d Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:26:25 -0600 Subject: [PATCH 17/31] bond summaries start --- migrations/1779487960678_bonds.ts | 21 +++++++++- src/api/routes/v3/staking-bonds.ts | 31 +++++++++++++- src/api/schemas/v3/cursors.ts | 6 +++ src/api/serializers/v3/bonds.ts | 44 ++++++++++++++++++++ src/datastore/common.ts | 1 + src/datastore/pg-store.ts | 1 + src/datastore/pg-write-store.ts | 25 +++++++++-- src/datastore/v3/constants.ts | 9 ++++ src/datastore/v3/pg-store-v3.ts | 66 +++++++++++++++++++++++++++++- src/datastore/v3/types.ts | 9 ++++ 10 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 src/api/serializers/v3/bonds.ts diff --git a/migrations/1779487960678_bonds.ts b/migrations/1779487960678_bonds.ts index 5d7b9b7ba..d11361ba8 100644 --- a/migrations/1779487960678_bonds.ts +++ b/migrations/1779487960678_bonds.ts @@ -67,7 +67,17 @@ export const up = (pgm: MigrationBuilder) => { early_unlock_admin: { type: 'text', notNull: true, - } + }, + allowed_count: { + type: 'integer', + notNull: true, + default: 0, + }, + registered_count: { + type: 'integer', + notNull: true, + default: 0, + }, }); pgm.createIndex( 'bonds', @@ -84,8 +94,17 @@ export const up = (pgm: MigrationBuilder) => { pgm.createIndex('bonds', 'bond_index', { where: 'canonical = TRUE AND microblock_canonical = TRUE', }); + + pgm.addColumn('chain_tip', { + bond_count: { + type: 'integer', + notNull: true, + default: 0, + }, + }); }; export const down = (pgm: MigrationBuilder) => { pgm.dropTable('bonds'); + pgm.dropColumn('chain_tip', 'bond_count'); }; diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index 7655bed7a..5622fb819 100644 --- a/src/api/routes/v3/staking-bonds.ts +++ b/src/api/routes/v3/staking-bonds.ts @@ -1,6 +1,15 @@ import { FastifyPluginAsync } from 'fastify'; import { Server } from 'node:http'; import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; +import { handleChainTipCache } from '../../controllers/cache-controller.js'; +import { getPagingQueryLimit, ResourceType } from '../../pagination.js'; +import { + BondCursorSchema, + CursorPaginatedResponse, + CursorPaginationQuerystring, +} from '../../schemas/v3/cursors.js'; +import { BondSummarySchema } from '../../schemas/v3/entities/bonds.js'; +import { serializeDbBondSummary } from '../../serializers/v3/bonds.js'; export const StakingBondsRoutes: FastifyPluginAsync< Record, @@ -10,15 +19,33 @@ export const StakingBondsRoutes: FastifyPluginAsync< fastify.get( '/staking/bonds', { + preHandler: handleChainTipCache, schema: { operationId: 'get_bonds', summary: 'Get bonds', description: 'Get bonds', tags: ['Staking'], + querystring: CursorPaginationQuerystring(BondCursorSchema, ResourceType.Tx), + response: { + 200: CursorPaginatedResponse(BondSummarySchema, BondCursorSchema, ResourceType.Tx), + }, }, }, - async (_req, reply) => { - await reply.send(); + async (req, reply) => { + const results = await fastify.db.v3.getBondSummaries({ + limit: req.query.limit ?? getPagingQueryLimit(ResourceType.Tx), + cursor: req.query.cursor, + }); + await reply.send({ + limit: results.limit, + total: results.total, + cursor: { + next: results.next_cursor, + previous: results.prev_cursor, + current: results.current_cursor, + }, + results: results.results.map(r => serializeDbBondSummary(r)), + }); } ); diff --git a/src/api/schemas/v3/cursors.ts b/src/api/schemas/v3/cursors.ts index 0f68ba2ef..61d2865d4 100644 --- a/src/api/schemas/v3/cursors.ts +++ b/src/api/schemas/v3/cursors.ts @@ -71,3 +71,9 @@ export const TransactionEventCursorSchema = Type.String({ description: 'Cursor for paginating transaction events. Format: event_index', }); export type TransactionEventCursor = Static; + +export const BondCursorSchema = Type.String({ + pattern: '^\\d+$', + description: 'Cursor for paginating bonds. Format: bond_index', +}); +export type BondCursor = Static; diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts new file mode 100644 index 000000000..475650038 --- /dev/null +++ b/src/api/serializers/v3/bonds.ts @@ -0,0 +1,44 @@ +import { DbBondSummary } from '../../../datastore/v3/types.js'; +import { BondSummary } from '../../schemas/v3/entities/bonds.js'; + +/** + * Serializes a database bond summary to a API bond summary. + * @param summary - The database bond summary to serialize. + * @returns The API bond summary. + */ +export function serializeDbBondSummary(summary: DbBondSummary): BondSummary { + return { + index: summary.bond_index, + pox_version: 'pox5', + status: 'upcoming', + parameters: { + target_rate_bps: summary.target_rate, + stx_value_ratio: summary.stx_value_ratio, + minimum_stx_ratio: summary.min_ustx_ratio, + btc_capacity: '0', + }, + registrations: { + allowed_count: summary.allowed_count, + registered_count: summary.registered_count, + }, + schedule: { + activation: { + bitcoin_height: 0, + pox_cycle: 0, + }, + unlock: { + bitcoin_height: 0, + pox_cycle: 0, + }, + }, + balances: { + locked: { + btc: '0', + stx: '0', + }, + paid_out: { + btc: '0', + }, + }, + }; +} diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 9f818dce1..2e708c103 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1617,6 +1617,7 @@ export interface DbChainTip { tx_count: number; tx_count_unanchored: number; mempool_tx_count: number; + bond_count: number; } export interface DbSmartContractStatus { diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 778dc761c..6ae54a34e 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -213,6 +213,7 @@ export class PgStore extends BasePgStore { tx_count: tip?.tx_count ?? 0, tx_count_unanchored: tip?.tx_count_unanchored ?? 0, mempool_tx_count: tip?.mempool_tx_count ?? 0, + bond_count: tip?.bond_count ?? 0, }; } diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 2a9bdd86e..d7db0add5 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -416,6 +416,12 @@ export class PgWriteStore extends PgStore { await sql` WITH new_tx_count AS ( SELECT tx_count + ${data.txs.length} AS tx_count FROM chain_tip + ), + new_bond_count AS ( + SELECT COUNT(*)::int AS bond_count + FROM bonds + WHERE canonical = true + AND microblock_canonical = true ) UPDATE chain_tip SET block_height = ${data.block.block_height}, @@ -426,7 +432,8 @@ export class PgWriteStore extends PgStore { microblock_sequence = NULL, block_count = ${data.block.block_height}, tx_count = (SELECT tx_count FROM new_tx_count), - tx_count_unanchored = (SELECT tx_count FROM new_tx_count) + tx_count_unanchored = (SELECT tx_count FROM new_tx_count), + bond_count = (SELECT bond_count FROM new_bond_count) `; if (this.metrics) { this.metrics.blockHeight.set(data.block.block_height); @@ -1101,7 +1108,13 @@ export class PgWriteStore extends PgStore { currentMicroblockTip.microblock_sequence === 0 ? sql`tx_count + ${data.txs.length}` : sql`tx_count_unanchored + ${data.txs.length}` - } + }, + bond_count = ( + SELECT COUNT(*)::int + FROM bonds + WHERE canonical = true + AND microblock_canonical = true + ) `; }); @@ -4260,7 +4273,13 @@ export class PgWriteStore extends PgStore { await sql` UPDATE chain_tip SET tx_count = tx_count + ${txCountDelta}, - tx_count_unanchored = tx_count_unanchored + ${txCountDelta} + tx_count_unanchored = tx_count_unanchored + ${txCountDelta}, + bond_count = ( + SELECT COUNT(*)::int + FROM bonds + WHERE canonical = true + AND microblock_canonical = true + ) `; } return updatedEntities; diff --git a/src/datastore/v3/constants.ts b/src/datastore/v3/constants.ts index 3dc7d489f..0ed6390b9 100644 --- a/src/datastore/v3/constants.ts +++ b/src/datastore/v3/constants.ts @@ -87,3 +87,12 @@ export const MEMPOOL_TX_COLUMNS = [ 'tenure_change_previous_tenure_blocks', 'tenure_change_pubkey_hash', ]; + +export const BOND_SUMMARY_COLUMNS = [ + 'bond_index', + 'target_rate', + 'stx_value_ratio', + 'min_ustx_ratio', + 'allowed_count', + 'registered_count', +]; diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index 3fb4a9503..917d0ce23 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -1,5 +1,6 @@ import { BasePgStoreModule } from '@stacks/api-toolkit'; import { + DbBondSummary, DbCursorPaginatedResult, DbMempoolTransaction, DbMempoolTransactionSummary, @@ -9,6 +10,7 @@ import { DbTransactionSummary, } from './types.js'; import { + BOND_SUMMARY_COLUMNS, MEMPOOL_TX_COLUMNS, MEMPOOL_TX_SUMMARY_COLUMNS, TX_COLUMNS, @@ -20,7 +22,11 @@ import { normalizeHashString } from '../../helpers.js'; import { BlockIdParam } from '../../api/routes/v2/schemas.js'; import { InvalidRequestError, InvalidRequestErrorType } from '../../errors.js'; import { TransactionIncludeField } from '../../api/schemas/v3/entities/transactions.js'; -import type { TransactionCursor, TransactionEventCursor } from '../../api/schemas/v3/cursors.js'; +import type { + BondCursor, + TransactionCursor, + TransactionEventCursor, +} from '../../api/schemas/v3/cursors.js'; import { encodeTransactionCursor, resolveTransactionCursor } from './helpers.js'; import { DbEventTypeId } from '../common.js'; @@ -641,4 +647,62 @@ export class PgStoreV3 extends BasePgStoreModule { }; }); } + + async getBondSummaries(args: { + limit: number; + cursor?: BondCursor; + }): Promise> { + return await this.sqlTransaction(async sql => { + const limit = args.limit; + const cursorFilter = args.cursor + ? sql`AND bond_index <= ${parseInt(args.cursor, 10)}` + : sql``; + + const totalQuery = await sql<{ total: number }[]>` + SELECT bond_count AS total + FROM chain_tip + `; + + const resultQuery = await sql` + SELECT ${sql(BOND_SUMMARY_COLUMNS)} + FROM bonds + WHERE canonical = true + AND microblock_canonical = true + ${cursorFilter} + ORDER BY bond_index DESC + LIMIT ${limit + 1} + `; + + const hasNextPage = resultQuery.count > limit; + const results = hasNextPage ? resultQuery.slice(0, limit) : resultQuery; + const firstResult = results[0]; + const extraResult = hasNextPage ? resultQuery[limit] : null; + + let prevCursor: string | null = null; + if (firstResult) { + const prevPageQuery = await sql[]>` + SELECT bond_index + FROM bonds + WHERE canonical = true + AND microblock_canonical = true + AND bond_index > ${firstResult.bond_index} + ORDER BY bond_index ASC + LIMIT ${limit} + `; + prevCursor = + prevPageQuery.length > 0 + ? prevPageQuery[prevPageQuery.length - 1].bond_index.toString() + : null; + } + + return { + limit, + next_cursor: extraResult ? extraResult.bond_index.toString() : null, + prev_cursor: prevCursor, + current_cursor: firstResult ? firstResult.bond_index.toString() : null, + total: totalQuery[0]?.total ?? 0, + results, + }; + }); + } } diff --git a/src/datastore/v3/types.ts b/src/datastore/v3/types.ts index 8b00c7d47..feb4d4d85 100644 --- a/src/datastore/v3/types.ts +++ b/src/datastore/v3/types.ts @@ -123,3 +123,12 @@ export interface DbTransactionEvent { memo: string | null; unlock_height: number | null; } + +export interface DbBondSummary { + bond_index: number; + target_rate: number; + stx_value_ratio: number; + min_ustx_ratio: number; + allowed_count: number; + registered_count: number; +} From 61e85a25581b1a9abfc8d76001139a16acd848ac Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:37:01 -0600 Subject: [PATCH 18/31] new bond fields --- migrations/1779487960678_bonds.ts | 22 ++++++++++++++++++++- src/api/routes/v3/staking-bonds.ts | 17 ++++++++++++---- src/api/schemas/v3/entities/bonds.ts | 1 - src/api/serializers/v3/bonds.ts | 29 ++++++++++++++++++++++++++-- src/datastore/common.ts | 6 +++++- src/datastore/pg-write-store.ts | 8 ++++++-- src/datastore/v3/constants.ts | 19 ++++++++++++++++++ src/datastore/v3/pg-store-v3.ts | 14 ++++++++++++++ src/datastore/v3/types.ts | 18 +++++++++++++++++ 9 files changed, 123 insertions(+), 11 deletions(-) diff --git a/migrations/1779487960678_bonds.ts b/migrations/1779487960678_bonds.ts index d11361ba8..3e7eefd93 100644 --- a/migrations/1779487960678_bonds.ts +++ b/migrations/1779487960678_bonds.ts @@ -60,7 +60,7 @@ export const up = (pgm: MigrationBuilder) => { type: 'integer', notNull: true, }, - early_unlock_signers: { + early_unlock_bytes: { type: 'text', notNull: true, }, @@ -68,6 +68,26 @@ export const up = (pgm: MigrationBuilder) => { type: 'text', notNull: true, }, + first_reward_cycle: { + type: 'integer', + notNull: true, + }, + bond_start_height: { + type: 'integer', + notNull: true, + }, + unlock_cycle: { + type: 'integer', + notNull: true, + }, + unlock_burn_height: { + type: 'integer', + notNull: true, + }, + btc_capacity: { + type: 'numeric', + notNull: true, + }, allowed_count: { type: 'integer', notNull: true, diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index 5622fb819..5f52317ac 100644 --- a/src/api/routes/v3/staking-bonds.ts +++ b/src/api/routes/v3/staking-bonds.ts @@ -8,8 +8,9 @@ import { CursorPaginatedResponse, CursorPaginationQuerystring, } from '../../schemas/v3/cursors.js'; -import { BondSummarySchema } from '../../schemas/v3/entities/bonds.js'; -import { serializeDbBondSummary } from '../../serializers/v3/bonds.js'; +import { BondSchema, BondSummarySchema } from '../../schemas/v3/entities/bonds.js'; +import { serializeDbBond, serializeDbBondSummary } from '../../serializers/v3/bonds.js'; +import { NotFoundError } from '../../../errors.js'; export const StakingBondsRoutes: FastifyPluginAsync< Record, @@ -52,6 +53,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< fastify.get( '/staking/bonds/:bond_index', { + preHandler: handleChainTipCache, schema: { operationId: 'get_bond', summary: 'Get bond', @@ -60,10 +62,17 @@ export const StakingBondsRoutes: FastifyPluginAsync< params: Type.Object({ bond_index: Type.Integer({ description: 'Bond index' }), }), + response: { + 200: BondSchema, + }, }, }, - async (_req, reply) => { - await reply.send(); + async (req, reply) => { + const bond = await fastify.db.v3.getBond({ bondIndex: req.params.bond_index }); + if (!bond) { + throw new NotFoundError('Bond not found'); + } + await reply.send(serializeDbBond(bond)); } ); diff --git a/src/api/schemas/v3/entities/bonds.ts b/src/api/schemas/v3/entities/bonds.ts index 972030b8d..307ca969c 100644 --- a/src/api/schemas/v3/entities/bonds.ts +++ b/src/api/schemas/v3/entities/bonds.ts @@ -81,7 +81,6 @@ export const BondSchema = Type.Composite([ block: BlockPositionSchema, bitcoin_block: BitcoinBlockPositionSchema, }), - early_unlock_signers: Type.String(), }), ]); export type Bond = Static; diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts index 475650038..09cb17e9a 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -1,5 +1,5 @@ -import { DbBondSummary } from '../../../datastore/v3/types.js'; -import { BondSummary } from '../../schemas/v3/entities/bonds.js'; +import { DbBond, DbBondSummary } from '../../../datastore/v3/types.js'; +import { Bond, BondSummary } from '../../schemas/v3/entities/bonds.js'; /** * Serializes a database bond summary to a API bond summary. @@ -42,3 +42,28 @@ export function serializeDbBondSummary(summary: DbBondSummary): BondSummary { }, }; } + +/** + * Serializes a database bond to a API bond. + * @param bond - The database bond to serialize. + * @returns The API bond. + */ +export function serializeDbBond(bond: DbBond): Bond { + return { + ...serializeDbBondSummary(bond), + transaction: { + tx_id: bond.tx_id, + block: { + height: bond.block_height, + hash: bond.block_hash, + index_hash: bond.index_block_hash, + time: bond.block_time, + tx_index: bond.tx_index, + }, + bitcoin_block: { + height: bond.burn_block_height, + time: bond.burn_block_time, + }, + }, + }; +} diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 2e708c103..8da40882f 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1644,7 +1644,11 @@ export interface DbBondInsertValues extends DbTxLocation { target_rate: number; stx_value_ratio: number; min_ustx_ratio: number; - early_unlock_signers: string; + first_reward_cycle: number; + bond_start_height: number; + unlock_cycle: number; + unlock_burn_height: number; + early_unlock_bytes: string; early_unlock_admin: string; } diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index d7db0add5..a67998f68 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -600,8 +600,12 @@ export class PgWriteStore extends PgStore { target_rate: parseInt(event.data.target_rate), stx_value_ratio: parseInt(event.data.stx_value_ratio), min_ustx_ratio: parseInt(event.data.min_ustx_ratio), - early_unlock_signers: event.data.early_unlock_signers, + early_unlock_bytes: event.data.early_unlock_bytes, early_unlock_admin: event.data.early_unlock_admin, + first_reward_cycle: parseInt(event.data.first_reward_cycle), + bond_start_height: parseInt(event.data.bond_start_height), + unlock_cycle: parseInt(event.data.unlock_cycle), + unlock_burn_height: parseInt(event.data.unlock_burn_height), }; await sql` INSERT INTO bonds ${sql(bond)} @@ -736,7 +740,7 @@ export class PgWriteStore extends PgStore { bond_index: parseInt(bondIndex), remaining_rewards: event.data.remaining_rewards, accrued_rewards: event.data.accrued_rewards, - new_reserve: event.data.new_reserve, + new_reserve: event.data.new_reserve ?? '0', stx_staker_rewards: event.data.stx_staker_rewards, stx_cycle: parseInt(event.data.stx_cycle), cycle_staked_ustx: event.data.cycle_staked_ustx, diff --git a/src/datastore/v3/constants.ts b/src/datastore/v3/constants.ts index 0ed6390b9..4c1573dd7 100644 --- a/src/datastore/v3/constants.ts +++ b/src/datastore/v3/constants.ts @@ -93,6 +93,25 @@ export const BOND_SUMMARY_COLUMNS = [ 'target_rate', 'stx_value_ratio', 'min_ustx_ratio', + 'first_reward_cycle', + 'bond_start_height', + 'unlock_cycle', + 'unlock_burn_height', + 'btc_capacity', 'allowed_count', 'registered_count', ]; + +export const BOND_COLUMNS = [ + ...BOND_SUMMARY_COLUMNS, + 'tx_id', + 'block_height', + 'block_hash', + 'index_block_hash', + 'block_time', + 'tx_index', + 'burn_block_height', + 'burn_block_time', + 'early_unlock_bytes', + 'early_unlock_admin', +]; diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index 917d0ce23..167bd98ef 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -1,5 +1,6 @@ import { BasePgStoreModule } from '@stacks/api-toolkit'; import { + DbBond, DbBondSummary, DbCursorPaginatedResult, DbMempoolTransaction, @@ -10,6 +11,7 @@ import { DbTransactionSummary, } from './types.js'; import { + BOND_COLUMNS, BOND_SUMMARY_COLUMNS, MEMPOOL_TX_COLUMNS, MEMPOOL_TX_SUMMARY_COLUMNS, @@ -705,4 +707,16 @@ export class PgStoreV3 extends BasePgStoreModule { }; }); } + + async getBond(args: { bondIndex: number }): Promise { + const result = await this.sql` + SELECT ${this.sql(BOND_COLUMNS)} + FROM bonds + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + LIMIT 1 + `; + return result[0] ?? null; + } } diff --git a/src/datastore/v3/types.ts b/src/datastore/v3/types.ts index feb4d4d85..95134fd8a 100644 --- a/src/datastore/v3/types.ts +++ b/src/datastore/v3/types.ts @@ -129,6 +129,24 @@ export interface DbBondSummary { target_rate: number; stx_value_ratio: number; min_ustx_ratio: number; + first_reward_cycle: number; + bond_start_height: number; + unlock_cycle: number; + unlock_burn_height: number; + btc_capacity: string; allowed_count: number; registered_count: number; } + +export interface DbBond extends DbBondSummary { + tx_id: string; + block_height: number; + block_hash: string; + index_block_hash: string; + block_time: number; + tx_index: number; + burn_block_height: number; + burn_block_time: number; + early_unlock_bytes: string; + early_unlock_admin: string; +} From bd3f2547d9e73520bddd8e92a74221a469234726 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:52:47 -0600 Subject: [PATCH 19/31] calculate bond paid out data --- migrations/1779487960678_bonds.ts | 15 +++ src/api/serializers/v3/bonds.ts | 18 ++-- src/datastore/pg-write-store.ts | 166 +++++++++++++++++++++++------- src/datastore/v3/constants.ts | 3 + src/datastore/v3/types.ts | 3 + 5 files changed, 161 insertions(+), 44 deletions(-) diff --git a/migrations/1779487960678_bonds.ts b/migrations/1779487960678_bonds.ts index 3e7eefd93..432995b06 100644 --- a/migrations/1779487960678_bonds.ts +++ b/migrations/1779487960678_bonds.ts @@ -88,6 +88,21 @@ export const up = (pgm: MigrationBuilder) => { type: 'numeric', notNull: true, }, + btc_locked: { + type: 'numeric', + notNull: true, + default: 0, + }, + stx_locked: { + type: 'numeric', + notNull: true, + default: 0, + }, + btc_paid_out: { + type: 'numeric', + notNull: true, + default: 0, + }, allowed_count: { type: 'integer', notNull: true, diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts index 09cb17e9a..9919c9c3c 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -10,12 +10,12 @@ export function serializeDbBondSummary(summary: DbBondSummary): BondSummary { return { index: summary.bond_index, pox_version: 'pox5', - status: 'upcoming', + status: 'upcoming', // TODO: Implement actual status logic parameters: { target_rate_bps: summary.target_rate, stx_value_ratio: summary.stx_value_ratio, minimum_stx_ratio: summary.min_ustx_ratio, - btc_capacity: '0', + btc_capacity: summary.btc_capacity, }, registrations: { allowed_count: summary.allowed_count, @@ -23,21 +23,21 @@ export function serializeDbBondSummary(summary: DbBondSummary): BondSummary { }, schedule: { activation: { - bitcoin_height: 0, - pox_cycle: 0, + bitcoin_height: summary.bond_start_height, + pox_cycle: summary.first_reward_cycle, }, unlock: { - bitcoin_height: 0, - pox_cycle: 0, + bitcoin_height: summary.unlock_burn_height, + pox_cycle: summary.unlock_cycle, }, }, balances: { locked: { - btc: '0', - stx: '0', + btc: summary.btc_locked, + stx: summary.stx_locked, }, paid_out: { - btc: '0', + btc: summary.btc_paid_out, }, }, }; diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index a67998f68..0b6d15a0f 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -670,10 +670,11 @@ export class PgWriteStore extends PgStore { switch (event.name) { case Pox5EventName.RegisterForBond: case Pox5EventName.UpdateBondRegistration: { + const bondIndex = parseInt(event.data.bond_index); const position: DbPrincipalBondPositionInsertValues = { ...txLocation, principal: event.data.staker, - bond_index: parseInt(event.data.bond_index), + bond_index: bondIndex, status: DbPrincipalBondPositionStatus.Enrolled, active: true, btc_locked: @@ -684,45 +685,129 @@ export class PgWriteStore extends PgStore { btc_paid_out: '0', }; await sql` - INSERT INTO principal_bond_positions ${sql(position)} - ON CONFLICT (principal, bond_index) DO UPDATE SET - tx_id = EXCLUDED.tx_id, - tx_index = EXCLUDED.tx_index, - block_height = EXCLUDED.block_height, - index_block_hash = EXCLUDED.index_block_hash, - parent_index_block_hash = EXCLUDED.parent_index_block_hash, - microblock_hash = EXCLUDED.microblock_hash, - microblock_sequence = EXCLUDED.microblock_sequence, - microblock_canonical = EXCLUDED.microblock_canonical, - canonical = EXCLUDED.canonical, - status = EXCLUDED.status, - active = EXCLUDED.active, - btc_locked = EXCLUDED.btc_locked, - stx_locked = EXCLUDED.stx_locked + WITH existing_position AS ( + SELECT btc_locked, stx_locked + FROM principal_bond_positions + WHERE principal = ${position.principal} + AND bond_index = ${bondIndex} + AND canonical = true + AND microblock_canonical = true + ), + upserted_position AS ( + INSERT INTO principal_bond_positions ${sql(position)} + ON CONFLICT (principal, bond_index) DO UPDATE SET + tx_id = EXCLUDED.tx_id, + tx_index = EXCLUDED.tx_index, + block_height = EXCLUDED.block_height, + index_block_hash = EXCLUDED.index_block_hash, + parent_index_block_hash = EXCLUDED.parent_index_block_hash, + microblock_hash = EXCLUDED.microblock_hash, + microblock_sequence = EXCLUDED.microblock_sequence, + microblock_canonical = EXCLUDED.microblock_canonical, + canonical = EXCLUDED.canonical, + status = EXCLUDED.status, + active = EXCLUDED.active, + btc_locked = EXCLUDED.btc_locked, + stx_locked = EXCLUDED.stx_locked + RETURNING btc_locked, stx_locked + ), + delta AS ( + SELECT + upserted_position.btc_locked::numeric + - COALESCE(existing_position.btc_locked::numeric, 0) AS btc_delta, + upserted_position.stx_locked::numeric + - COALESCE(existing_position.stx_locked::numeric, 0) AS stx_delta + FROM upserted_position + LEFT JOIN existing_position ON true + ) + UPDATE bonds + SET + btc_locked = bonds.btc_locked + delta.btc_delta, + stx_locked = bonds.stx_locked + delta.stx_delta + FROM delta + WHERE bonds.bond_index = ${bondIndex} + AND bonds.canonical = true + AND bonds.microblock_canonical = true `; break; } case Pox5EventName.AnnounceL1EarlyExit: await sql` - UPDATE principal_bond_positions SET - status = ${DbPrincipalBondPositionStatus.EarlyExit}, - active = false - WHERE principal = ${event.data.staker} - AND bond_index = ${parseInt(event.data.bond_index)} - AND canonical = true - AND microblock_canonical = true - `; + WITH updated_position AS ( + UPDATE principal_bond_positions + SET + status = ${DbPrincipalBondPositionStatus.EarlyExit}, + active = false + WHERE principal = ${event.data.staker} + AND bond_index = ${parseInt(event.data.bond_index)} + AND canonical = true + AND microblock_canonical = true + RETURNING + bond_index, + btc_locked::numeric AS previous_btc_locked, + stx_locked::numeric AS previous_stx_locked, + btc_locked::numeric AS new_btc_locked, + stx_locked::numeric AS new_stx_locked + ), + delta AS ( + SELECT + SUM(new_btc_locked - previous_btc_locked) AS btc_delta, + SUM(new_stx_locked - previous_stx_locked) AS stx_delta, + bond_index + FROM updated_position + GROUP BY bond_index + ) + UPDATE bonds + SET + btc_locked = bonds.btc_locked + delta.btc_delta, + stx_locked = bonds.stx_locked + delta.stx_delta + FROM delta + WHERE bonds.bond_index = delta.bond_index + AND bonds.canonical = true + AND bonds.microblock_canonical = true + `; return; case Pox5EventName.UnstakeSbtc: await sql` - UPDATE principal_bond_positions SET - ${event.data.new_amount_sats == '0' ? sql`status = ${DbPrincipalBondPositionStatus.EarlyExit}` : sql``} - btc_locked = ${event.data.new_amount_sats} - WHERE principal = ${event.data.staker} - AND bond_index = ${parseInt(event.data.bond_index)} - AND canonical = true - AND microblock_canonical = true - `; + WITH existing_position AS ( + SELECT bond_index, btc_locked::numeric AS btc_locked, stx_locked::numeric AS stx_locked + FROM principal_bond_positions + WHERE principal = ${event.data.staker} + AND bond_index = ${parseInt(event.data.bond_index)} + AND canonical = true + AND microblock_canonical = true + ), + updated_position AS ( + UPDATE principal_bond_positions + SET + ${event.data.new_amount_sats == '0' ? sql`status = ${DbPrincipalBondPositionStatus.EarlyExit},` : sql``} + btc_locked = ${event.data.new_amount_sats} + WHERE principal = ${event.data.staker} + AND bond_index = ${parseInt(event.data.bond_index)} + AND canonical = true + AND microblock_canonical = true + RETURNING + bond_index, + btc_locked::numeric AS new_btc_locked, + stx_locked::numeric AS new_stx_locked + ), + delta AS ( + SELECT + updated_position.new_btc_locked - existing_position.btc_locked AS btc_delta, + updated_position.new_stx_locked - existing_position.stx_locked AS stx_delta, + updated_position.bond_index + FROM updated_position + INNER JOIN existing_position USING (bond_index) + ) + UPDATE bonds + SET + btc_locked = bonds.btc_locked + delta.btc_delta, + stx_locked = bonds.stx_locked + delta.stx_delta + FROM delta + WHERE bonds.bond_index = delta.bond_index + AND bonds.canonical = true + AND bonds.microblock_canonical = true + `; return; } } @@ -759,14 +844,25 @@ export class PgWriteStore extends PgStore { txLocation: DbTxLocation, event: Pox5EventAddToAllowlist ) { + const bondIndex = parseInt(event.data.bond_index); + const maxSats = event.data.max_sats; const bondAllowlistEntry: DbBondAllowlistEntryInsertValues = { ...txLocation, - bond_index: parseInt(event.data.bond_index), + bond_index: bondIndex, staker: event.data.staker, - max_sats: event.data.max_sats, + max_sats: maxSats, }; await sql` - INSERT INTO bond_allowlist_entries ${sql(bondAllowlistEntry)} + WITH inserted AS ( + INSERT INTO bond_allowlist_entries ${sql(bondAllowlistEntry)} + RETURNING bond_index, max_sats + ) + UPDATE bonds AS b + SET btc_capacity = b.btc_capacity + i.max_sats::numeric + FROM inserted AS i + WHERE b.bond_index = i.bond_index + AND b.canonical = true + AND b.microblock_canonical = true `; } diff --git a/src/datastore/v3/constants.ts b/src/datastore/v3/constants.ts index 4c1573dd7..dbcf9b5a8 100644 --- a/src/datastore/v3/constants.ts +++ b/src/datastore/v3/constants.ts @@ -98,6 +98,9 @@ export const BOND_SUMMARY_COLUMNS = [ 'unlock_cycle', 'unlock_burn_height', 'btc_capacity', + 'btc_locked', + 'stx_locked', + 'btc_paid_out', 'allowed_count', 'registered_count', ]; diff --git a/src/datastore/v3/types.ts b/src/datastore/v3/types.ts index 95134fd8a..c92a9ad61 100644 --- a/src/datastore/v3/types.ts +++ b/src/datastore/v3/types.ts @@ -134,6 +134,9 @@ export interface DbBondSummary { unlock_cycle: number; unlock_burn_height: number; btc_capacity: string; + btc_locked: string; + stx_locked: string; + btc_paid_out: string; allowed_count: number; registered_count: number; } From ff8b19395c4f7cd9a363f198ebe1a8564e91772c Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:01:52 -0600 Subject: [PATCH 20/31] bond status --- src/api/routes/v3/staking-bonds.ts | 4 ++-- src/api/serializers/v3/bonds.ts | 22 +++++++++++++++---- src/datastore/v3/pg-store-v3.ts | 35 ++++++++++++++++++++---------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index 5f52317ac..43e19384b 100644 --- a/src/api/routes/v3/staking-bonds.ts +++ b/src/api/routes/v3/staking-bonds.ts @@ -45,7 +45,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< previous: results.prev_cursor, current: results.current_cursor, }, - results: results.results.map(r => serializeDbBondSummary(r)), + results: results.results.map(r => serializeDbBondSummary(r, results.burn_block_height)), }); } ); @@ -72,7 +72,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< if (!bond) { throw new NotFoundError('Bond not found'); } - await reply.send(serializeDbBond(bond)); + await reply.send(serializeDbBond(bond, bond.burn_block_height)); } ); diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts index 9919c9c3c..98cca3b5b 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -1,16 +1,30 @@ import { DbBond, DbBondSummary } from '../../../datastore/v3/types.js'; import { Bond, BondSummary } from '../../schemas/v3/entities/bonds.js'; +import { BondStatus } from '../../schemas/v3/entities/bonds.js'; + +function getBondStatus(summary: DbBondSummary, currentBurnBlockHeight: number): BondStatus { + if (currentBurnBlockHeight < summary.bond_start_height) { + return 'upcoming'; + } + if (currentBurnBlockHeight < summary.unlock_burn_height) { + return 'active'; + } + return 'unlocked'; +} /** * Serializes a database bond summary to a API bond summary. * @param summary - The database bond summary to serialize. * @returns The API bond summary. */ -export function serializeDbBondSummary(summary: DbBondSummary): BondSummary { +export function serializeDbBondSummary( + summary: DbBondSummary, + currentBurnBlockHeight: number +): BondSummary { return { index: summary.bond_index, pox_version: 'pox5', - status: 'upcoming', // TODO: Implement actual status logic + status: getBondStatus(summary, currentBurnBlockHeight), parameters: { target_rate_bps: summary.target_rate, stx_value_ratio: summary.stx_value_ratio, @@ -48,9 +62,9 @@ export function serializeDbBondSummary(summary: DbBondSummary): BondSummary { * @param bond - The database bond to serialize. * @returns The API bond. */ -export function serializeDbBond(bond: DbBond): Bond { +export function serializeDbBond(bond: DbBond, currentBurnBlockHeight: number): Bond { return { - ...serializeDbBondSummary(bond), + ...serializeDbBondSummary(bond, currentBurnBlockHeight), transaction: { tx_id: bond.tx_id, block: { diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index 167bd98ef..afc007f3d 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -653,7 +653,7 @@ export class PgStoreV3 extends BasePgStoreModule { async getBondSummaries(args: { limit: number; cursor?: BondCursor; - }): Promise> { + }): Promise & { burn_block_height: number }> { return await this.sqlTransaction(async sql => { const limit = args.limit; const cursorFilter = args.cursor @@ -697,6 +697,9 @@ export class PgStoreV3 extends BasePgStoreModule { : null; } + const chainTip = await sql<{ burn_block_height: number }[]>` + SELECT burn_block_height FROM chain_tip LIMIT 1 + `; return { limit, next_cursor: extraResult ? extraResult.bond_index.toString() : null, @@ -704,19 +707,29 @@ export class PgStoreV3 extends BasePgStoreModule { current_cursor: firstResult ? firstResult.bond_index.toString() : null, total: totalQuery[0]?.total ?? 0, results, + burn_block_height: chainTip[0]?.burn_block_height ?? 0, }; }); } - async getBond(args: { bondIndex: number }): Promise { - const result = await this.sql` - SELECT ${this.sql(BOND_COLUMNS)} - FROM bonds - WHERE canonical = true - AND microblock_canonical = true - AND bond_index = ${args.bondIndex} - LIMIT 1 - `; - return result[0] ?? null; + async getBond(args: { + bondIndex: number; + }): Promise<(DbBond & { burn_block_height: number }) | null> { + return await this.sqlTransaction(async sql => { + const chainTip = await sql<{ burn_block_height: number }[]>` + SELECT burn_block_height FROM chain_tip LIMIT 1 + `; + const result = await sql` + SELECT ${this.sql(BOND_COLUMNS)} + FROM bonds + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + LIMIT 1 + `; + return result[0] + ? { ...result[0], burn_block_height: chainTip[0]?.burn_block_height ?? 0 } + : null; + }); } } From 1c31a10a62567677b58011815a93d492db90cea4 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:46:16 -0600 Subject: [PATCH 21/31] bond allowlist --- src/api/routes/v3/staking-bonds.ts | 38 +++++++++++- src/api/serializers/v3/bonds.ts | 10 ++- src/datastore/v3/constants.ts | 8 +++ src/datastore/v3/pg-store-v3.ts | 98 ++++++++++++++++++++++++++++++ src/datastore/v3/types.ts | 5 ++ 5 files changed, 155 insertions(+), 4 deletions(-) diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index 43e19384b..183c8ce1e 100644 --- a/src/api/routes/v3/staking-bonds.ts +++ b/src/api/routes/v3/staking-bonds.ts @@ -7,9 +7,15 @@ import { BondCursorSchema, CursorPaginatedResponse, CursorPaginationQuerystring, + TransactionCursorSchema, } from '../../schemas/v3/cursors.js'; import { BondSchema, BondSummarySchema } from '../../schemas/v3/entities/bonds.js'; -import { serializeDbBond, serializeDbBondSummary } from '../../serializers/v3/bonds.js'; +import { BondAllowlistSchema } from '../../schemas/v3/entities/bond-allowlist-entries.js'; +import { + serializeDbBond, + serializeDbBondAllowlistEntry, + serializeDbBondSummary, +} from '../../serializers/v3/bonds.js'; import { NotFoundError } from '../../../errors.js'; export const StakingBondsRoutes: FastifyPluginAsync< @@ -79,15 +85,41 @@ export const StakingBondsRoutes: FastifyPluginAsync< fastify.get( '/staking/bonds/:bond_index/allowlist', { + preHandler: handleChainTipCache, schema: { operationId: 'get_bond_allowlist_entries', summary: 'Get bond allowlist entries', description: 'Get bond allowlist entries', tags: ['Staking'], + params: Type.Object({ + bond_index: Type.Integer({ description: 'Bond index' }), + }), + querystring: CursorPaginationQuerystring(TransactionCursorSchema, ResourceType.Tx), + response: { + 200: CursorPaginatedResponse( + BondAllowlistSchema, + TransactionCursorSchema, + ResourceType.Tx + ), + }, }, }, - async (_req, reply) => { - await reply.send(); + async (req, reply) => { + const results = await fastify.db.v3.getBondAllowlistEntries({ + bondIndex: req.params.bond_index, + limit: req.query.limit ?? getPagingQueryLimit(ResourceType.Tx), + cursor: req.query.cursor, + }); + await reply.send({ + limit: results.limit, + total: results.total, + cursor: { + next: results.next_cursor, + previous: results.prev_cursor, + current: results.current_cursor, + }, + results: results.results.map(r => serializeDbBondAllowlistEntry(r)), + }); } ); diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts index 98cca3b5b..69408b3d5 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -1,6 +1,7 @@ -import { DbBond, DbBondSummary } from '../../../datastore/v3/types.js'; +import { DbBond, DbBondAllowlistEntry, DbBondSummary } from '../../../datastore/v3/types.js'; import { Bond, BondSummary } from '../../schemas/v3/entities/bonds.js'; import { BondStatus } from '../../schemas/v3/entities/bonds.js'; +import { BondAllowlist } from '../../schemas/v3/entities/bond-allowlist-entries.js'; function getBondStatus(summary: DbBondSummary, currentBurnBlockHeight: number): BondStatus { if (currentBurnBlockHeight < summary.bond_start_height) { @@ -81,3 +82,10 @@ export function serializeDbBond(bond: DbBond, currentBurnBlockHeight: number): B }, }; } + +export function serializeDbBondAllowlistEntry(entry: DbBondAllowlistEntry): BondAllowlist { + return { + staker: entry.staker, + max_sats: entry.max_sats, + }; +} diff --git a/src/datastore/v3/constants.ts b/src/datastore/v3/constants.ts index dbcf9b5a8..c89db85a0 100644 --- a/src/datastore/v3/constants.ts +++ b/src/datastore/v3/constants.ts @@ -118,3 +118,11 @@ export const BOND_COLUMNS = [ 'early_unlock_bytes', 'early_unlock_admin', ]; + +export const BOND_ALLOWLIST_ENTRY_COLUMNS = [ + 'staker', + 'max_sats', + 'block_height', + 'microblock_sequence', + 'tx_index', +]; diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index afc007f3d..3bbbb34ef 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -1,6 +1,7 @@ import { BasePgStoreModule } from '@stacks/api-toolkit'; import { DbBond, + DbBondAllowlistEntry, DbBondSummary, DbCursorPaginatedResult, DbMempoolTransaction, @@ -11,6 +12,7 @@ import { DbTransactionSummary, } from './types.js'; import { + BOND_ALLOWLIST_ENTRY_COLUMNS, BOND_COLUMNS, BOND_SUMMARY_COLUMNS, MEMPOOL_TX_COLUMNS, @@ -732,4 +734,100 @@ export class PgStoreV3 extends BasePgStoreModule { : null; }); } + + async getBondAllowlistEntries(args: { + bondIndex: number; + limit: number; + cursor?: TransactionCursor; + }): Promise> { + return await this.sqlTransaction(async sql => { + const limit = args.limit; + let cursorFilter = sql``; + if (args.cursor) { + const cursor = await resolveTransactionCursor(args.cursor, async cursor => { + const exactCursorQuery = await sql<{ exists: boolean }[]>` + SELECT EXISTS ( + SELECT 1 + FROM bond_allowlist_entries + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + AND (block_height, microblock_sequence, tx_index) + = (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) + ) AS exists + `; + return exactCursorQuery[0]?.exists ?? false; + }); + cursorFilter = sql` + AND (block_height, microblock_sequence, tx_index) + <= (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) + `; + } + + const totalQuery = await sql<{ total: number }[]>` + SELECT allowed_count AS total + FROM bonds + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + LIMIT 1 + `; + + const resultQuery = await sql< + (DbBondAllowlistEntry & { + block_height: number; + microblock_sequence: number; + tx_index: number; + })[] + >` + SELECT ${sql(BOND_ALLOWLIST_ENTRY_COLUMNS)} + FROM bond_allowlist_entries + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + ${cursorFilter} + ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC + LIMIT ${limit + 1} + `; + + const hasNextPage = resultQuery.count > limit; + const results = hasNextPage ? resultQuery.slice(0, limit) : resultQuery; + const firstResult = results[0]; + const extraResult = hasNextPage ? resultQuery[limit] : null; + + let prevCursor: string | null = null; + if (firstResult) { + const prevPageQuery = await sql< + { block_height: number; microblock_sequence: number; tx_index: number }[] + >` + SELECT block_height, microblock_sequence, tx_index + FROM bond_allowlist_entries + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + AND (block_height, microblock_sequence, tx_index) + > ( + ${firstResult.block_height}, + ${firstResult.microblock_sequence}, + ${firstResult.tx_index} + ) + ORDER BY block_height ASC, microblock_sequence ASC, tx_index ASC + LIMIT ${limit} + `; + prevCursor = + prevPageQuery.length > 0 + ? encodeTransactionCursor(prevPageQuery[prevPageQuery.length - 1]) + : null; + } + + return { + limit, + next_cursor: extraResult ? encodeTransactionCursor(extraResult) : null, + prev_cursor: prevCursor, + current_cursor: firstResult ? encodeTransactionCursor(firstResult) : null, + total: totalQuery[0]?.total ?? 0, + results, + }; + }); + } } diff --git a/src/datastore/v3/types.ts b/src/datastore/v3/types.ts index c92a9ad61..6d1e5d61f 100644 --- a/src/datastore/v3/types.ts +++ b/src/datastore/v3/types.ts @@ -153,3 +153,8 @@ export interface DbBond extends DbBondSummary { early_unlock_bytes: string; early_unlock_admin: string; } + +export interface DbBondAllowlistEntry { + staker: string; + max_sats: string; +} From 2de70a5fe1035183fd19083aaa122b5475adc658 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:02:26 -0600 Subject: [PATCH 22/31] allowlist entry for principal --- src/api/routes/v3/staking-bonds.ts | 20 ++++++++++++++-- src/datastore/v3/pg-store-v3.ts | 38 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index 183c8ce1e..d6e14d9f0 100644 --- a/src/api/routes/v3/staking-bonds.ts +++ b/src/api/routes/v3/staking-bonds.ts @@ -11,6 +11,7 @@ import { } from '../../schemas/v3/cursors.js'; import { BondSchema, BondSummarySchema } from '../../schemas/v3/entities/bonds.js'; import { BondAllowlistSchema } from '../../schemas/v3/entities/bond-allowlist-entries.js'; +import { PrincipalSchema } from '../../schemas/v3/entities/common.js'; import { serializeDbBond, serializeDbBondAllowlistEntry, @@ -126,15 +127,30 @@ export const StakingBondsRoutes: FastifyPluginAsync< fastify.get( '/staking/bonds/:bond_index/allowlist/:principal', { + preHandler: handleChainTipCache, schema: { operationId: 'get_bond_allowlist_entry', summary: 'Get bond allowlist entry', description: 'Get bond allowlist entry', tags: ['Staking'], + params: Type.Object({ + bond_index: Type.Integer({ description: 'Bond index' }), + principal: PrincipalSchema, + }), + response: { + 200: BondAllowlistSchema, + }, }, }, - async (_req, reply) => { - await reply.send(); + async (req, reply) => { + const entry = await fastify.db.v3.getBondAllowlistEntry({ + bondIndex: req.params.bond_index, + principal: req.params.principal, + }); + if (!entry) { + throw new NotFoundError('Bond allowlist entry not found'); + } + await reply.send(serializeDbBondAllowlistEntry(entry)); } ); diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index 3bbbb34ef..3111882c8 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -652,6 +652,11 @@ export class PgStoreV3 extends BasePgStoreModule { }); } + /** + * Gets the summaries for all bonds. + * @param args - The arguments for the query. + * @returns The summaries for all bonds. + */ async getBondSummaries(args: { limit: number; cursor?: BondCursor; @@ -714,6 +719,11 @@ export class PgStoreV3 extends BasePgStoreModule { }); } + /** + * Gets a bond by index. + * @param args - The arguments for the query. + * @returns The bond by index. + */ async getBond(args: { bondIndex: number; }): Promise<(DbBond & { burn_block_height: number }) | null> { @@ -735,6 +745,11 @@ export class PgStoreV3 extends BasePgStoreModule { }); } + /** + * Gets the allowlist entries for a bond. + * @param args - The arguments for the query. + * @returns The allowlist entries for a bond. + */ async getBondAllowlistEntries(args: { bondIndex: number; limit: number; @@ -830,4 +845,27 @@ export class PgStoreV3 extends BasePgStoreModule { }; }); } + + /** + * Gets an allowlist entry for a bond and principal. + * @param args - The arguments for the query. + * @returns The allowlist entry for a bond and principal. + */ + async getBondAllowlistEntry(args: { + bondIndex: number; + principal: Principal; + }): Promise { + return await this.sqlTransaction(async sql => { + const result = await sql` + SELECT staker, max_sats + FROM bond_allowlist_entries + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + AND staker = ${args.principal} + LIMIT 1 + `; + return result[0] ?? null; + }); + } } From 1798f10166d439f373ed365d2515e1450c56e1b7 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:46:10 -0600 Subject: [PATCH 23/31] registrations endpoints --- src/api/routes/v3/staking-bonds.ts | 67 +++++++-- .../schemas/v3/entities/bond-registrations.ts | 35 ++--- src/api/serializers/v3/bonds.ts | 22 ++- src/datastore/v3/constants.ts | 15 ++ src/datastore/v3/pg-store-v3.ts | 131 ++++++++++++++++-- src/datastore/v3/types.ts | 18 +++ 6 files changed, 240 insertions(+), 48 deletions(-) diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index d6e14d9f0..a6576091f 100644 --- a/src/api/routes/v3/staking-bonds.ts +++ b/src/api/routes/v3/staking-bonds.ts @@ -11,10 +11,12 @@ import { } from '../../schemas/v3/cursors.js'; import { BondSchema, BondSummarySchema } from '../../schemas/v3/entities/bonds.js'; import { BondAllowlistSchema } from '../../schemas/v3/entities/bond-allowlist-entries.js'; -import { PrincipalSchema } from '../../schemas/v3/entities/common.js'; +import { BondRegistrationSchema } from '../../schemas/v3/entities/bond-registrations.js'; +import { BondIndexSchema, PrincipalSchema } from '../../schemas/v3/entities/common.js'; import { serializeDbBond, serializeDbBondAllowlistEntry, + serializeDbBondRegistration, serializeDbBondSummary, } from '../../serializers/v3/bonds.js'; import { NotFoundError } from '../../../errors.js'; @@ -67,7 +69,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< description: 'Get bond', tags: ['Staking'], params: Type.Object({ - bond_index: Type.Integer({ description: 'Bond index' }), + bond_index: BondIndexSchema, }), response: { 200: BondSchema, @@ -93,7 +95,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< description: 'Get bond allowlist entries', tags: ['Staking'], params: Type.Object({ - bond_index: Type.Integer({ description: 'Bond index' }), + bond_index: BondIndexSchema, }), querystring: CursorPaginationQuerystring(TransactionCursorSchema, ResourceType.Tx), response: { @@ -134,7 +136,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< description: 'Get bond allowlist entry', tags: ['Staking'], params: Type.Object({ - bond_index: Type.Integer({ description: 'Bond index' }), + bond_index: BondIndexSchema, principal: PrincipalSchema, }), response: { @@ -157,30 +159,71 @@ export const StakingBondsRoutes: FastifyPluginAsync< fastify.get( '/staking/bonds/:bond_index/registrations', { + preHandler: handleChainTipCache, schema: { - operationId: 'get_pox5_bond_allowlist_entries', - summary: 'Get PoX-5 bond allowlist entries', - description: 'Get PoX-5 bond allowlist entries', - tags: ['PoX-5'], + operationId: 'get_bond_registrations', + summary: 'Get bond registrations', + description: 'Get bond registrations', + tags: ['Staking'], + params: Type.Object({ + bond_index: BondIndexSchema, + }), + querystring: CursorPaginationQuerystring(TransactionCursorSchema, ResourceType.Tx), + response: { + 200: CursorPaginatedResponse( + BondRegistrationSchema, + TransactionCursorSchema, + ResourceType.Tx + ), + }, }, }, - async (_req, reply) => { - await reply.send(); + async (req, reply) => { + const results = await fastify.db.v3.getBondRegistrations({ + bondIndex: req.params.bond_index, + limit: req.query.limit ?? getPagingQueryLimit(ResourceType.Tx), + cursor: req.query.cursor, + }); + await reply.send({ + limit: results.limit, + total: results.total, + cursor: { + next: results.next_cursor, + previous: results.prev_cursor, + current: results.current_cursor, + }, + results: results.results.map(r => serializeDbBondRegistration(r)), + }); } ); fastify.get( '/staking/bonds/:bond_index/registrations/:principal', { + preHandler: handleChainTipCache, schema: { operationId: 'get_bond_registration', summary: 'Get bond registration', description: 'Get bond registration', tags: ['Staking'], + params: Type.Object({ + bond_index: BondIndexSchema, + principal: PrincipalSchema, + }), + response: { + 200: BondRegistrationSchema, + }, }, }, - async (_req, reply) => { - await reply.send(); + async (req, reply) => { + const registration = await fastify.db.v3.getBondRegistration({ + bondIndex: req.params.bond_index, + principal: req.params.principal, + }); + if (!registration) { + throw new NotFoundError('Bond registration not found'); + } + await reply.send(serializeDbBondRegistration(registration)); } ); diff --git a/src/api/schemas/v3/entities/bond-registrations.ts b/src/api/schemas/v3/entities/bond-registrations.ts index 40ae44ee1..b54b32233 100644 --- a/src/api/schemas/v3/entities/bond-registrations.ts +++ b/src/api/schemas/v3/entities/bond-registrations.ts @@ -1,29 +1,14 @@ import { Static, Type } from '@sinclair/typebox'; -export const Pox5BondRegistrationSchema = Type.Object({ +export const BondRegistrationSchema = Type.Object({ bond_index: Type.Integer(), - pox_address: Type.Optional( - Type.String({ - description: - 'Where they want to receive BTC rewards. If this is none, rewards are received as sBTC.', - }) - ), - signer_manager: Type.String(), - btc_lockup: Type.Union([ - Type.Object({ - type: Type.Literal('outputs'), - outputs: Type.Object({ - amount: Type.String(), - tx_id: Type.String(), - output_index: Type.Integer(), - }), - unlock_bytes: Type.String(), - }), - Type.Object({ - type: Type.Literal('sbtc'), - amount: Type.String(), - }), - ]), - signer_calldata: Type.Optional(Type.String()), + signer: Type.String(), + staker: Type.String(), + amount_ustx: Type.String(), + sats_total: Type.String(), + first_reward_cycle: Type.Integer(), + unlock_burn_height: Type.Integer(), + unlock_cycle: Type.Integer(), + is_l1_lock: Type.Boolean(), }); -export type Pox5BondRegistration = Static; +export type BondRegistration = Static; diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts index 69408b3d5..548a89b71 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -1,7 +1,13 @@ -import { DbBond, DbBondAllowlistEntry, DbBondSummary } from '../../../datastore/v3/types.js'; +import { + DbBond, + DbBondAllowlistEntry, + DbBondRegistration, + DbBondSummary, +} from '../../../datastore/v3/types.js'; import { Bond, BondSummary } from '../../schemas/v3/entities/bonds.js'; import { BondStatus } from '../../schemas/v3/entities/bonds.js'; import { BondAllowlist } from '../../schemas/v3/entities/bond-allowlist-entries.js'; +import { BondRegistration } from '../../schemas/v3/entities/bond-registrations.js'; function getBondStatus(summary: DbBondSummary, currentBurnBlockHeight: number): BondStatus { if (currentBurnBlockHeight < summary.bond_start_height) { @@ -89,3 +95,17 @@ export function serializeDbBondAllowlistEntry(entry: DbBondAllowlistEntry): Bond max_sats: entry.max_sats, }; } + +export function serializeDbBondRegistration(entry: DbBondRegistration): BondRegistration { + return { + bond_index: entry.bond_index, + signer: entry.signer, + staker: entry.staker, + amount_ustx: entry.amount_ustx, + sats_total: entry.sats_total, + first_reward_cycle: entry.first_reward_cycle, + unlock_burn_height: entry.unlock_burn_height, + unlock_cycle: entry.unlock_cycle, + is_l1_lock: entry.is_l1_lock, + }; +} diff --git a/src/datastore/v3/constants.ts b/src/datastore/v3/constants.ts index c89db85a0..b0b49a715 100644 --- a/src/datastore/v3/constants.ts +++ b/src/datastore/v3/constants.ts @@ -126,3 +126,18 @@ export const BOND_ALLOWLIST_ENTRY_COLUMNS = [ 'microblock_sequence', 'tx_index', ]; + +export const BOND_REGISTRATION_COLUMNS = [ + 'bond_index', + 'signer', + 'staker', + 'amount_ustx', + 'sats_total', + 'first_reward_cycle', + 'unlock_burn_height', + 'unlock_cycle', + 'is_l1_lock', + 'block_height', + 'microblock_sequence', + 'tx_index', +]; diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index 3111882c8..3029a46a0 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -2,17 +2,20 @@ import { BasePgStoreModule } from '@stacks/api-toolkit'; import { DbBond, DbBondAllowlistEntry, + DbBondRegistration, DbBondSummary, DbCursorPaginatedResult, DbMempoolTransaction, DbMempoolTransactionSummary, DbPrincipalTransactionSummary, DbTransaction, + DbTransactionCursor, DbTransactionEvent, DbTransactionSummary, } from './types.js'; import { BOND_ALLOWLIST_ENTRY_COLUMNS, + BOND_REGISTRATION_COLUMNS, BOND_COLUMNS, BOND_SUMMARY_COLUMNS, MEMPOOL_TX_COLUMNS, @@ -788,13 +791,7 @@ export class PgStoreV3 extends BasePgStoreModule { LIMIT 1 `; - const resultQuery = await sql< - (DbBondAllowlistEntry & { - block_height: number; - microblock_sequence: number; - tx_index: number; - })[] - >` + const resultQuery = await sql<(DbBondAllowlistEntry & DbTransactionCursor)[]>` SELECT ${sql(BOND_ALLOWLIST_ENTRY_COLUMNS)} FROM bond_allowlist_entries WHERE canonical = true @@ -812,9 +809,7 @@ export class PgStoreV3 extends BasePgStoreModule { let prevCursor: string | null = null; if (firstResult) { - const prevPageQuery = await sql< - { block_height: number; microblock_sequence: number; tx_index: number }[] - >` + const prevPageQuery = await sql` SELECT block_height, microblock_sequence, tx_index FROM bond_allowlist_entries WHERE canonical = true @@ -868,4 +863,120 @@ export class PgStoreV3 extends BasePgStoreModule { return result[0] ?? null; }); } + + /** + * Gets the registrations for a bond. + * @param args - The arguments for the query. + * @returns The registrations for a bond. + */ + async getBondRegistrations(args: { + bondIndex: number; + limit: number; + cursor?: TransactionCursor; + }): Promise> { + return await this.sqlTransaction(async sql => { + const limit = args.limit; + let cursorFilter = sql``; + if (args.cursor) { + const cursor = await resolveTransactionCursor(args.cursor, async cursor => { + const exactCursorQuery = await sql<{ exists: boolean }[]>` + SELECT EXISTS ( + SELECT 1 + FROM bond_registrations + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + AND (block_height, microblock_sequence, tx_index) + = (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) + ) AS exists + `; + return exactCursorQuery[0]?.exists ?? false; + }); + cursorFilter = sql` + AND (block_height, microblock_sequence, tx_index) + <= (${cursor.block_height}, ${cursor.microblock_sequence}, ${cursor.tx_index}) + `; + } + + const totalQuery = await sql<{ total: number }[]>` + SELECT registered_count AS total + FROM bonds + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + LIMIT 1 + `; + + const resultQuery = await sql<(DbBondRegistration & DbTransactionCursor)[]>` + SELECT ${sql(BOND_REGISTRATION_COLUMNS)} + FROM bond_registrations + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + ${cursorFilter} + ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC + LIMIT ${limit + 1} + `; + + const hasNextPage = resultQuery.count > limit; + const results = hasNextPage ? resultQuery.slice(0, limit) : resultQuery; + const firstResult = results[0]; + const extraResult = hasNextPage ? resultQuery[limit] : null; + + let prevCursor: string | null = null; + if (firstResult) { + const prevPageQuery = await sql` + SELECT block_height, microblock_sequence, tx_index + FROM bond_registrations + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + AND (block_height, microblock_sequence, tx_index) + > ( + ${firstResult.block_height}, + ${firstResult.microblock_sequence}, + ${firstResult.tx_index} + ) + ORDER BY block_height ASC, microblock_sequence ASC, tx_index ASC + LIMIT ${limit} + `; + prevCursor = + prevPageQuery.length > 0 + ? encodeTransactionCursor(prevPageQuery[prevPageQuery.length - 1]) + : null; + } + + return { + limit, + next_cursor: extraResult ? encodeTransactionCursor(extraResult) : null, + prev_cursor: prevCursor, + current_cursor: firstResult ? encodeTransactionCursor(firstResult) : null, + total: totalQuery[0]?.total ?? 0, + results, + }; + }); + } + + /** + * Gets a registration for a bond and principal. + * @param args - The arguments for the query. + * @returns The latest registration for a bond and principal. + */ + async getBondRegistration(args: { + bondIndex: number; + principal: Principal; + }): Promise { + return await this.sqlTransaction(async sql => { + const result = await sql` + SELECT ${sql(BOND_REGISTRATION_COLUMNS)} + FROM bond_registrations + WHERE canonical = true + AND microblock_canonical = true + AND bond_index = ${args.bondIndex} + AND staker = ${args.principal} + LIMIT 1 + `; + return result[0] ?? null; + }); + } } diff --git a/src/datastore/v3/types.ts b/src/datastore/v3/types.ts index 6d1e5d61f..434358aad 100644 --- a/src/datastore/v3/types.ts +++ b/src/datastore/v3/types.ts @@ -158,3 +158,21 @@ export interface DbBondAllowlistEntry { staker: string; max_sats: string; } + +export interface DbBondRegistration { + bond_index: number; + signer: string; + staker: string; + amount_ustx: string; + sats_total: string; + first_reward_cycle: number; + unlock_burn_height: number; + unlock_cycle: number; + is_l1_lock: boolean; +} + +export interface DbTransactionCursor { + block_height: number; + microblock_sequence: number; + tx_index: number; +} From 2a9253eb8a6f0f794d34e1caafb21a10aa2a7051 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:22:35 -0600 Subject: [PATCH 24/31] add pox4 unlock height --- migrations/1779392293803_pox5-events.ts | 10 ++++++++++ migrations/1779487960678_bonds.ts | 2 +- migrations/1779487971367_bond-allowlist-entries.ts | 1 - src/datastore/common.ts | 1 + src/datastore/pg-write-store.ts | 8 ++++++++ src/event-stream/event-server.ts | 2 ++ 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/migrations/1779392293803_pox5-events.ts b/migrations/1779392293803_pox5-events.ts index f0fa36285..d7634f1c5 100644 --- a/migrations/1779392293803_pox5-events.ts +++ b/migrations/1779392293803_pox5-events.ts @@ -71,8 +71,18 @@ export const up = (pgm: MigrationBuilder) => { pgm.createIndex('pox5_events', 'tx_id'); pgm.createIndex('pox5_events', ['index_block_hash', 'canonical']); pgm.createIndex('pox5_events', 'microblock_hash'); + + // Add pox_v5_unlock_height to pox_state table to track the unlock height for pox v5 + pgm.addColumn('pox_state', { + pox_v4_unlock_height: { + type: 'bigint', + notNull: true, + default: 0, + }, + }); }; export const down = (pgm: MigrationBuilder) => { pgm.dropTable('pox5_events'); + pgm.dropColumn('pox_state', 'pox_v4_unlock_height'); }; diff --git a/migrations/1779487960678_bonds.ts b/migrations/1779487960678_bonds.ts index 432995b06..57660b7a0 100644 --- a/migrations/1779487960678_bonds.ts +++ b/migrations/1779487960678_bonds.ts @@ -120,7 +120,6 @@ export const up = (pgm: MigrationBuilder) => { { name: 'block_height', sort: 'DESC' }, { name: 'microblock_sequence', sort: 'DESC' }, { name: 'tx_index', sort: 'DESC' }, - { name: 'event_index', sort: 'DESC' }, ], { where: 'canonical = TRUE AND microblock_canonical = TRUE', @@ -130,6 +129,7 @@ export const up = (pgm: MigrationBuilder) => { where: 'canonical = TRUE AND microblock_canonical = TRUE', }); + // Add bond count to chain tip table to track the number of bonds in the chain pgm.addColumn('chain_tip', { bond_count: { type: 'integer', diff --git a/migrations/1779487971367_bond-allowlist-entries.ts b/migrations/1779487971367_bond-allowlist-entries.ts index 02b502e62..2d670425c 100644 --- a/migrations/1779487971367_bond-allowlist-entries.ts +++ b/migrations/1779487971367_bond-allowlist-entries.ts @@ -64,7 +64,6 @@ export const up = (pgm: MigrationBuilder) => { { name: 'block_height', sort: 'DESC' }, { name: 'microblock_sequence', sort: 'DESC' }, { name: 'tx_index', sort: 'DESC' }, - { name: 'event_index', sort: 'DESC' }, ], { where: 'canonical = TRUE AND microblock_canonical = TRUE', diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 8da40882f..59e155fe2 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -508,6 +508,7 @@ export interface DataStoreBlockUpdateData { pox_v1_unlock_height?: number; pox_v2_unlock_height?: number; pox_v3_unlock_height?: number; + pox_v4_unlock_height?: number; poxSetSigners?: DbPoxSetSigners; } diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 0b6d15a0f..47c730495 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -891,6 +891,14 @@ export class PgWriteStore extends PgStore { WHERE pox_v3_unlock_height != ${data.pox_v3_unlock_height} `; } + if (data.pox_v4_unlock_height !== undefined) { + // update the pox_state.pox_v4_unlock_height singleton + await sql` + UPDATE pox_state + SET pox_v4_unlock_height = ${data.pox_v4_unlock_height} + WHERE pox_v4_unlock_height != ${data.pox_v4_unlock_height} + `; + } } async updateMinerRewards(sql: PgSqlClient, minerRewards: DbMinerReward[]): Promise { diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index 2698afc03..78962648e 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -1208,6 +1208,8 @@ export function parseNewBlockMessage( pox_v1_unlock_height: msg.pox_v1_unlock_height, pox_v2_unlock_height: msg.pox_v2_unlock_height, pox_v3_unlock_height: msg.pox_v3_unlock_height, + pox_v4_unlock_height: (msg as NewBlockMessage & { pox_v4_unlock_height?: number }) + .pox_v4_unlock_height, poxSetSigners: poxSetSigners, }; From a0b01753821cdb18a266c69a49c508c2dea54eae Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:35:36 -0600 Subject: [PATCH 25/31] pox4 boundary unlocks --- src/api/routes/v1/status.ts | 30 ++-- src/api/schemas/v1/responses/responses.ts | 3 - src/datastore/pg-store-v2.ts | 66 +++---- src/datastore/pg-store.ts | 210 ++++++++++++++-------- src/event-stream/event-server.ts | 3 +- 5 files changed, 181 insertions(+), 131 deletions(-) diff --git a/src/api/routes/v1/status.ts b/src/api/routes/v1/status.ts index 0fa75d1d4..e991a050d 100644 --- a/src/api/routes/v1/status.ts +++ b/src/api/routes/v1/status.ts @@ -31,25 +31,17 @@ export const StatusRoutes: FastifyPluginAsync< status: 'ready', }; try { - await fastify.db.sqlTransaction(async sql => { - const poxForceUnlockHeights = await fastify.db.getPoxForcedUnlockHeightsInternal(sql); - if (poxForceUnlockHeights.found) { - response.pox_v1_unlock_height = poxForceUnlockHeights.result.pox1UnlockHeight as number; - response.pox_v2_unlock_height = poxForceUnlockHeights.result.pox2UnlockHeight as number; - response.pox_v3_unlock_height = poxForceUnlockHeights.result.pox3UnlockHeight as number; - } - const chainTip = await fastify.db.getChainTip(sql); - if (chainTip.block_height > 0) { - response.chain_tip = { - block_height: chainTip.block_height, - block_hash: chainTip.block_hash, - index_block_hash: chainTip.index_block_hash, - microblock_hash: chainTip.microblock_hash, - microblock_sequence: chainTip.microblock_sequence, - burn_block_height: chainTip.burn_block_height, - }; - } - }); + const chainTip = await fastify.db.getChainTip(fastify.db.sql); + if (chainTip.block_height > 0) { + response.chain_tip = { + block_height: chainTip.block_height, + block_hash: chainTip.block_hash, + index_block_hash: chainTip.index_block_hash, + microblock_hash: chainTip.microblock_hash, + microblock_sequence: chainTip.microblock_sequence, + burn_block_height: chainTip.burn_block_height, + }; + } } catch (_error) { // ignore error } diff --git a/src/api/schemas/v1/responses/responses.ts b/src/api/schemas/v1/responses/responses.ts index a4060ccb4..f806fc357 100644 --- a/src/api/schemas/v1/responses/responses.ts +++ b/src/api/schemas/v1/responses/responses.ts @@ -33,9 +33,6 @@ export const ServerStatusResponseSchema = Type.Object( status: Type.String({ description: 'the current server status', }), - pox_v1_unlock_height: OptionalNullable(Type.Integer()), - pox_v2_unlock_height: OptionalNullable(Type.Integer()), - pox_v3_unlock_height: OptionalNullable(Type.Integer()), chain_tip: OptionalNullable( Type.Object({ block_height: Type.Integer({ diff --git a/src/datastore/pg-store-v2.ts b/src/datastore/pg-store-v2.ts index 35e7e4d52..d8b13ed1a 100644 --- a/src/datastore/pg-store-v2.ts +++ b/src/datastore/pg-store-v2.ts @@ -1183,39 +1183,45 @@ export class PgStoreV2 extends BasePgStoreModule { let burnchainLockHeight = 0; let burnchainUnlockHeight = 0; - // == PoX-4 ================================================================ + const [poxState] = await sql<{ pox_v4_unlock_height: string }[]>` + SELECT pox_v4_unlock_height + FROM pox_state + LIMIT 1 + `; + const pox4UnlockHeight = parseInt(poxState?.pox_v4_unlock_height ?? '0') || null; + const includePox4State = !pox4UnlockHeight || burnBlockHeight <= pox4UnlockHeight; + // Query for the latest lock event that still applies to the current burn block height. // Special case for `handle-unlock` which should be returned if it is the last received event. + if (includePox4State) { + const pox4EventQuery = await sql` + SELECT ${sql(POX4_SYNTHETIC_EVENT_COLUMNS)} + FROM pox4_events + WHERE canonical = true AND microblock_canonical = true AND stacker = ${stxAddress} + AND block_height <= ${blockHeight} + AND ( + (name != ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height >= ${burnBlockHeight}) + OR + (name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${burnBlockHeight}) + ) + ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC + LIMIT 1 + `; + if (pox4EventQuery.length > 0) { + const pox4Event = parseDbPoxSyntheticEvent(pox4EventQuery[0]); + if (pox4Event.name !== Pox4EventName.HandleUnlock) { + lockTxId = pox4Event.tx_id; + locked = BigInt(pox4Event.locked); + burnchainUnlockHeight = Number(pox4Event.burnchain_unlock_height); + lockHeight = pox4Event.block_height; - const pox4EventQuery = await sql` - SELECT ${sql(POX4_SYNTHETIC_EVENT_COLUMNS)} - FROM pox4_events - WHERE canonical = true AND microblock_canonical = true AND stacker = ${stxAddress} - AND block_height <= ${blockHeight} - AND ( - (name != ${ - Pox4EventName.HandleUnlock - } AND burnchain_unlock_height >= ${burnBlockHeight}) - OR - (name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${burnBlockHeight}) - ) - ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC - LIMIT 1 - `; - if (pox4EventQuery.length > 0) { - const pox4Event = parseDbPoxSyntheticEvent(pox4EventQuery[0]); - if (pox4Event.name !== Pox4EventName.HandleUnlock) { - lockTxId = pox4Event.tx_id; - locked = BigInt(pox4Event.locked); - burnchainUnlockHeight = Number(pox4Event.burnchain_unlock_height); - lockHeight = pox4Event.block_height; - - const [burnBlockQuery] = await sql<{ burn_block_height: string }[]>` - SELECT burn_block_height FROM blocks - WHERE block_height = ${blockHeight} AND canonical = true - LIMIT 1 - `; - burnchainLockHeight = parseInt(burnBlockQuery?.burn_block_height ?? '0'); + const [burnBlockQuery] = await sql<{ burn_block_height: string }[]>` + SELECT burn_block_height FROM blocks + WHERE block_height = ${blockHeight} AND canonical = true + LIMIT 1 + `; + burnchainLockHeight = parseInt(burnBlockQuery?.burn_block_height ?? '0'); + } } } diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 6ae54a34e..30c213802 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -290,14 +290,20 @@ export class PgStore extends BasePgStore { pox1UnlockHeight: number | null; pox2UnlockHeight: number | null; pox3UnlockHeight: number | null; + pox4UnlockHeight: number | null; }> > { const query = await sql< - { pox_v1_unlock_height: string; pox_v2_unlock_height: string; pox_v3_unlock_height: string }[] + { + pox_v1_unlock_height: string; + pox_v2_unlock_height: string; + pox_v3_unlock_height: string; + pox_v4_unlock_height: string; + }[] >` - SELECT pox_v1_unlock_height, pox_v2_unlock_height, pox_v3_unlock_height + SELECT pox_v1_unlock_height, pox_v2_unlock_height, pox_v3_unlock_height, pox_v4_unlock_height FROM pox_state - LIMIt 1 + LIMIT 1 `; if (query.length === 0) { return { found: false }; @@ -305,10 +311,14 @@ export class PgStore extends BasePgStore { const pox1UnlockHeight = parseInt(query[0].pox_v1_unlock_height) || null; const pox2UnlockHeight = parseInt(query[0].pox_v2_unlock_height) || null; const pox3UnlockHeight = parseInt(query[0].pox_v3_unlock_height) || null; + const pox4UnlockHeight = parseInt(query[0].pox_v4_unlock_height) || null; if (pox2UnlockHeight === 0) { return { found: false }; } - return { found: true, result: { pox1UnlockHeight, pox2UnlockHeight, pox3UnlockHeight } }; + return { + found: true, + result: { pox1UnlockHeight, pox2UnlockHeight, pox3UnlockHeight, pox4UnlockHeight }, + }; } async getPoxForceUnlockHeights() { @@ -2274,6 +2284,7 @@ export class PgStore extends BasePgStore { let includePox1State = true; let includePox2State = true; let includePox3State = true; + let includePox4State = true; const poxForceUnlockHeights = await this.getPoxForcedUnlockHeightsInternal(sql); if (poxForceUnlockHeights.found) { if ( @@ -2294,6 +2305,12 @@ export class PgStore extends BasePgStore { ) { includePox3State = false; } + if ( + poxForceUnlockHeights.result.pox4UnlockHeight && + burnBlockHeight > poxForceUnlockHeights.result.pox4UnlockHeight + ) { + includePox4State = false; + } } // Once the pox_v1_unlock_height is reached, stop using `stx_lock_events` to determinel locked state, @@ -2402,44 +2419,42 @@ export class PgStore extends BasePgStore { } } - // == PoX-4 ================================================================ - // Assuming includePox3State = true; since there is no unlock height for pox4 (yet) - - // Query for the latest lock event that still applies to the current burn block height. - // Special case for `handle-unlock` which should be returned if it is the last received event. - - const pox4EventQuery = await sql` - SELECT ${sql(POX4_SYNTHETIC_EVENT_COLUMNS)} - FROM pox4_events - WHERE canonical = true AND microblock_canonical = true AND stacker = ${stxAddress} - AND block_height <= ${blockHeight} - AND ( - (name != ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height >= ${burnBlockHeight}) - OR - (name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${burnBlockHeight}) - ) - ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC - LIMIT 1 - `; - if (pox4EventQuery.length > 0) { - const pox4Event = parseDbPoxSyntheticEvent(pox4EventQuery[0]); - if (pox4Event.name === Pox4EventName.HandleUnlock) { - // on a handle-unlock, set all of the locked stx related property to empty/default - lockTxId = ''; - locked = 0n; - burnchainUnlockHeight = 0; - lockHeight = 0; - burnchainLockHeight = 0; - } else { - lockTxId = pox4Event.tx_id; - locked = BigInt(pox4Event.locked); - burnchainUnlockHeight = Number(pox4Event.burnchain_unlock_height); - lockHeight = pox4Event.block_height; - const blockQuery = await this.getBlockByHeightInternal(sql, lockHeight); - burnchainLockHeight = blockQuery.found ? blockQuery.result.burn_block_height : 0; + // Once the pox_v4_unlock_height is reached, stop using `pox4_events` to determine locked state. + if (includePox4State) { + // Query for the latest lock event that still applies to the current burn block height. + // Special case for `handle-unlock` which should be returned if it is the last received event. + const pox4EventQuery = await sql` + SELECT ${sql(POX4_SYNTHETIC_EVENT_COLUMNS)} + FROM pox4_events + WHERE canonical = true AND microblock_canonical = true AND stacker = ${stxAddress} + AND block_height <= ${blockHeight} + AND ( + (name != ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height >= ${burnBlockHeight}) + OR + (name = ${Pox4EventName.HandleUnlock} AND burnchain_unlock_height < ${burnBlockHeight}) + ) + ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC + LIMIT 1 + `; + if (pox4EventQuery.length > 0) { + const pox4Event = parseDbPoxSyntheticEvent(pox4EventQuery[0]); + if (pox4Event.name === Pox4EventName.HandleUnlock) { + // on a handle-unlock, set all of the locked stx related property to empty/default + lockTxId = ''; + locked = 0n; + burnchainUnlockHeight = 0; + lockHeight = 0; + burnchainLockHeight = 0; + } else { + lockTxId = pox4Event.tx_id; + locked = BigInt(pox4Event.locked); + burnchainUnlockHeight = Number(pox4Event.burnchain_unlock_height); + lockHeight = pox4Event.block_height; + const blockQuery = await this.getBlockByHeightInternal(sql, lockHeight); + burnchainLockHeight = blockQuery.found ? blockQuery.result.burn_block_height : 0; + } } } - // ========================================================================= const minerRewardQuery = await sql<{ amount: string }[]>` SELECT sum( @@ -4146,11 +4161,13 @@ export class PgStore extends BasePgStore { let v1UnlockHeight: number | null = null; let v2UnlockHeight: number | null = null; let v3UnlockHeight: number | null = null; + let v4UnlockHeight: number | null = null; const poxUnlockHeights = await this.getPoxForcedUnlockHeightsInternal(sql); if (poxUnlockHeights.found) { v1UnlockHeight = poxUnlockHeights.result.pox1UnlockHeight; v2UnlockHeight = poxUnlockHeights.result.pox2UnlockHeight; v3UnlockHeight = poxUnlockHeights.result.pox3UnlockHeight; + v4UnlockHeight = poxUnlockHeights.result.pox4UnlockHeight; } type StxLockEventResult = { @@ -4363,45 +4380,83 @@ export class PgStore extends BasePgStore { } } - // eslint-disable-next-line no-useless-assignment let poxV4Unlocks: StxLockEventResult[] = []; - const pox4EventQuery = await sql` - SELECT DISTINCT ON (stacker) stacker, ${sql(POX4_SYNTHETIC_EVENT_COLUMNS)} - FROM pox4_events - WHERE canonical = true AND microblock_canonical = true - AND block_height <= ${block.block_height} - AND ( - ( - burnchain_unlock_height <= ${current_burn_height} - AND burnchain_unlock_height > ${previous_burn_height} - AND name IN ${sql([ - Pox4EventName.StackStx, - Pox4EventName.StackIncrease, - Pox4EventName.StackExtend, - Pox4EventName.DelegateStackStx, - Pox4EventName.DelegateStackIncrease, - Pox4EventName.DelegateStackExtend, - ])} - ) OR ( - name = ${Pox4EventName.HandleUnlock} - AND burnchain_unlock_height < ${current_burn_height} - AND burnchain_unlock_height >= ${previous_burn_height} + const checkPox4Unlocks = v4UnlockHeight === null || current_burn_height < v4UnlockHeight; + if (checkPox4Unlocks) { + const pox4EventQuery = await sql` + SELECT DISTINCT ON (stacker) stacker, ${sql(POX4_SYNTHETIC_EVENT_COLUMNS)} + FROM pox4_events + WHERE canonical = true AND microblock_canonical = true + AND block_height <= ${block.block_height} + AND ( + ( + burnchain_unlock_height <= ${current_burn_height} + AND burnchain_unlock_height > ${previous_burn_height} + AND name IN ${sql([ + Pox4EventName.StackStx, + Pox4EventName.StackIncrease, + Pox4EventName.StackExtend, + Pox4EventName.DelegateStackStx, + Pox4EventName.DelegateStackIncrease, + Pox4EventName.DelegateStackExtend, + ])} + ) OR ( + name = ${Pox4EventName.HandleUnlock} + AND burnchain_unlock_height < ${current_burn_height} + AND burnchain_unlock_height >= ${previous_burn_height} + ) ) - ) - ORDER BY stacker, block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC - `; - poxV4Unlocks = pox4EventQuery.map(row => { - const pox4Event = parseDbPoxSyntheticEvent(row); - const unlockEvent: StxLockEventResult = { - locked_amount: pox4Event.locked.toString(), - unlock_height: Number(pox4Event.burnchain_unlock_height), - locked_address: pox4Event.stacker, - block_height: pox4Event.block_height, - tx_index: pox4Event.tx_index, - event_index: pox4Event.event_index, - }; - return unlockEvent; - }); + ORDER BY stacker, block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC + `; + poxV4Unlocks = pox4EventQuery.map(row => { + const pox4Event = parseDbPoxSyntheticEvent(row); + const unlockEvent: StxLockEventResult = { + locked_amount: pox4Event.locked.toString(), + unlock_height: Number(pox4Event.burnchain_unlock_height), + locked_address: pox4Event.stacker, + block_height: pox4Event.block_height, + tx_index: pox4Event.tx_index, + event_index: pox4Event.event_index, + }; + return unlockEvent; + }); + } + + const poxV4ForceUnlocks: StxLockEventResult[] = []; + const generatePoxV4ForceUnlocks = + v4UnlockHeight !== null && + current_burn_height > v4UnlockHeight && + previous_burn_height <= v4UnlockHeight; + if (generatePoxV4ForceUnlocks) { + const pox4EventQuery = await sql` + SELECT DISTINCT ON (stacker) stacker, ${sql(POX4_SYNTHETIC_EVENT_COLUMNS)} + FROM pox4_events + WHERE canonical = true AND microblock_canonical = true + AND block_height <= ${block.block_height} + AND ( + ( name != ${Pox4EventName.HandleUnlock} AND + burnchain_unlock_height >= ${current_burn_height}) + OR + ( name = ${Pox4EventName.HandleUnlock} AND + burnchain_unlock_height < ${current_burn_height}) + ) + ORDER BY stacker, block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC + `; + for (const row of pox4EventQuery) { + const pox4Event = parseDbPoxSyntheticEvent(row); + if (pox4Event.name !== Pox4EventName.HandleUnlock) { + const unlockEvent: StxLockEventResult = { + locked_amount: pox4Event.locked.toString(), + unlock_height: Number(pox4Event.burnchain_unlock_height), + locked_address: pox4Event.stacker, + block_height: pox4Event.block_height, + tx_index: pox4Event.tx_index, + event_index: pox4Event.event_index, + }; + poxV4ForceUnlocks.push(unlockEvent); + } + } + } const txIdQuery = await sql<{ tx_id: string }[]>` SELECT tx_id @@ -4420,6 +4475,7 @@ export class PgStore extends BasePgStore { poxV3Unlocks, poxV3ForceUnlocks, poxV4Unlocks, + poxV4ForceUnlocks, ]) { unlocks.forEach(row => { const unlockEvent: StxUnlockEvent = { diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index 78962648e..d9b2f6780 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -1208,8 +1208,7 @@ export function parseNewBlockMessage( pox_v1_unlock_height: msg.pox_v1_unlock_height, pox_v2_unlock_height: msg.pox_v2_unlock_height, pox_v3_unlock_height: msg.pox_v3_unlock_height, - pox_v4_unlock_height: (msg as NewBlockMessage & { pox_v4_unlock_height?: number }) - .pox_v4_unlock_height, + pox_v4_unlock_height: msg.pox_v4_unlock_height, poxSetSigners: poxSetSigners, }; From 90dc5e9202ae59e6eff11c15b1e36f87d753b9cc Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:43:22 -0600 Subject: [PATCH 26/31] update snp --- .vscode/launch.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ed21c4327..735ac1c3e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "outputCapture": "std", "internalConsoleOptions": "openOnSessionStart", "env": { - "NODE_ENV": "development" + "NODE_ENV": "production" } }, { diff --git a/package-lock.json b/package-lock.json index 0aeb1a3db..f4d7ee0d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", - "@stacks/node-publisher-client": "2.1.3", + "@stacks/node-publisher-client": "2.2.0", "@stacks/stacking": "7.4.0", "@stacks/transactions": "7.4.0", "bignumber.js": "10.0.2", @@ -1773,9 +1773,9 @@ } }, "node_modules/@stacks/node-publisher-client": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@stacks/node-publisher-client/-/node-publisher-client-2.1.3.tgz", - "integrity": "sha512-aq4HjqHtPkDhDYM8IeVnnUuUhals7DQDSSBCuro1zaPixN2xHbatBcvtPV8evJukCnM2zt5vHpDHF8CG2/O8Kw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@stacks/node-publisher-client/-/node-publisher-client-2.2.0.tgz", + "integrity": "sha512-/O/udLosZGQRyfjrYQ9mIymYDZSQncKlheCpbNsFG8oAoZrNOR8TjiFzLhaRGci4d1FmWPvQU6H9xRrkXBbwsQ==", "license": "GPL-3.0-only", "dependencies": { "@stacks/api-toolkit": "^1.13.0", diff --git a/package.json b/package.json index f52009fec..27e6c2aa5 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", - "@stacks/node-publisher-client": "2.1.3", + "@stacks/node-publisher-client": "2.2.0", "@stacks/stacking": "7.4.0", "@stacks/transactions": "7.4.0", "bignumber.js": "10.0.2", From 237c61566b91d50e38a9fecb37117869f7a88e1f Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:27:05 -0600 Subject: [PATCH 27/31] upgrade codec --- package-lock.json | 45 +++++++++++++++----------------- package.json | 2 +- src/event-stream/event-server.ts | 4 ++- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4d7ee0d7..11026ea0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@fastify/type-provider-typebox": "5.2.0", "@sinclair/typebox": "0.34.48", "@stacks/api-toolkit": "1.13.0", - "@stacks/codec": "file:../stacks-codec-js", + "@stacks/codec": "2.0.0-pox5.1", "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", @@ -73,27 +73,6 @@ "node": ">=24" } }, - "../stacks-codec-js": { - "name": "@stacks/codec", - "version": "1.8.0-pox5.1", - "license": "GPL-3.0", - "dependencies": { - "@types/node": "^24.0.0", - "detect-libc": "^2.0.1" - }, - "devDependencies": { - "@stacks/prettier-config": "^0.0.10", - "@types/jest": "^29.5.14", - "cargo-cp-artifact": "^0.1.9", - "esbuild": "^0.25.0", - "jest": "^29.7.0", - "ts-jest": "^29.3.0", - "typescript": "^5.7.0" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1720,8 +1699,17 @@ "license": "MIT" }, "node_modules/@stacks/codec": { - "resolved": "../stacks-codec-js", - "link": true + "version": "2.0.0-pox5.1", + "resolved": "https://registry.npmjs.org/@stacks/codec/-/codec-2.0.0-pox5.1.tgz", + "integrity": "sha512-EVQ8+BR70GGy7HSaVq4Z1YQdgZods8FwFqwRgUcDAi2mO2DYUty6OUDgrWaQ+/AfTNaAzGAKqsFK0dhBIp+zaA==", + "license": "GPL-3.0", + "dependencies": { + "@types/node": "^24.0.0", + "detect-libc": "^2.0.1" + }, + "engines": { + "node": ">=20" + } }, "node_modules/@stacks/common": { "version": "7.3.1", @@ -3401,6 +3389,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", diff --git a/package.json b/package.json index 27e6c2aa5..f33a51141 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@fastify/type-provider-typebox": "5.2.0", "@sinclair/typebox": "0.34.48", "@stacks/api-toolkit": "1.13.0", - "@stacks/codec": "file:../stacks-codec-js", + "@stacks/codec": "2.0.0-pox5.1", "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index d9b2f6780..f6d5766d0 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -1194,7 +1194,9 @@ export function parseNewBlockMessage( } poxSetSigners = { cycle_number: msg.cycle_number, - pox_ustx_threshold: BigInt(msg.reward_set.pox_ustx_threshold), + pox_ustx_threshold: msg.reward_set.pox_ustx_threshold + ? BigInt(msg.reward_set.pox_ustx_threshold) + : 50_000n * 1_000_000n, // 50,000 STX signers, rewarded_addresses: rewardedAddresses, }; From f61bce9d5e3e12a12bff9e1b46fdc2acf55f67e2 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:28:39 -0600 Subject: [PATCH 28/31] fix client version merge --- client/package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/package.json b/client/package.json index 31a2b2f41..00ab57fa1 100644 --- a/client/package.json +++ b/client/package.json @@ -1,10 +1,6 @@ { "name": "@stacks/blockchain-api-client", -<<<<<<< HEAD "version": "9.0.0-pox5.1", -======= - "version": "9.0.0-next.36", ->>>>>>> next "access": "public", "description": "Client for the Stacks Blockchain API", "homepage": "https://github.com/hirosystems/stacks-blockchain-api/tree/master/client#readme", From a096a7425eb2e7e2bc48608330aed543fcc3e1fa Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:37:40 -0600 Subject: [PATCH 29/31] fix ds test --- tests/api/datastore/datastore.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/api/datastore/datastore.test.ts b/tests/api/datastore/datastore.test.ts index 7ee43fe3f..0aca348b6 100644 --- a/tests/api/datastore/datastore.test.ts +++ b/tests/api/datastore/datastore.test.ts @@ -4492,6 +4492,7 @@ describe('postgres datastore', () => { index_block_hash: '0xcc', burn_block_height: 123, block_count: 3, + bond_count: 0, mempool_tx_count: 0, microblock_count: 0, microblock_hash: undefined, @@ -4565,6 +4566,7 @@ describe('postgres datastore', () => { index_block_hash: '0xcc', burn_block_height: 123, block_count: 3, + bond_count: 0, microblock_count: 0, microblock_hash: undefined, microblock_sequence: undefined, @@ -4620,6 +4622,7 @@ describe('postgres datastore', () => { block_hash: '0x44bb', block_height: 4, burn_block_height: 123, + bond_count: 0, index_block_hash: '0xddbb', microblock_count: 0, microblock_hash: undefined, From 5e2571acde5eac887ae5ac65507ddddeb2750bad Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:56:31 -0600 Subject: [PATCH 30/31] fix pox4 test --- .../pox-4-burnchain-delegate-stx.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/krypton/pox-4-burnchain-delegate-stx/pox-4-burnchain-delegate-stx.test.ts b/tests/krypton/pox-4-burnchain-delegate-stx/pox-4-burnchain-delegate-stx.test.ts index ed5a4ccc3..8e3ae14cf 100644 --- a/tests/krypton/pox-4-burnchain-delegate-stx/pox-4-burnchain-delegate-stx.test.ts +++ b/tests/krypton/pox-4-burnchain-delegate-stx/pox-4-burnchain-delegate-stx.test.ts @@ -428,7 +428,7 @@ describe('PoX-4 - Stack using Bitcoin-chain delegate ops', () => { assert.ok(res !== undefined); assert.equal(res.results.length, 1); assert.equal(res.results[0].name, 'delegate-stack-stx'); - assert.equal(res.results[0].pox_addr, poxAddrPayoutAccount.btcTestnetAddr); + assert.equal(res.results[0].pox_addr, poxAddrPayoutAccount.btcAddr); assert.equal(res.results[0].stacker, account.stxAddr); assert.equal(res.results[0].balance, BigInt(coreBalanceInfo.balance).toString()); assert.equal(res.results[0].locked, testStackAmount.toString()); From 8cb53b51b4bfb6db29295cfc7d4fe84c40944718 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:40:26 -0600 Subject: [PATCH 31/31] fix esm imports in migrations --- migrations/1779487960678_bonds.ts | 2 +- migrations/1779487971367_bond-allowlist-entries.ts | 2 +- migrations/1779487975550_bond-registrations.ts | 2 +- migrations/1779742831642_principal-bond-positions.ts | 2 +- migrations/1779745340752_bond-reward-distributions.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/migrations/1779487960678_bonds.ts b/migrations/1779487960678_bonds.ts index 57660b7a0..de829384c 100644 --- a/migrations/1779487960678_bonds.ts +++ b/migrations/1779487960678_bonds.ts @@ -1,4 +1,4 @@ -import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; +import type { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; diff --git a/migrations/1779487971367_bond-allowlist-entries.ts b/migrations/1779487971367_bond-allowlist-entries.ts index 2d670425c..2d0df0ea7 100644 --- a/migrations/1779487971367_bond-allowlist-entries.ts +++ b/migrations/1779487971367_bond-allowlist-entries.ts @@ -1,4 +1,4 @@ -import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; +import type { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; diff --git a/migrations/1779487975550_bond-registrations.ts b/migrations/1779487975550_bond-registrations.ts index fcf60d1db..d8ad35bed 100644 --- a/migrations/1779487975550_bond-registrations.ts +++ b/migrations/1779487975550_bond-registrations.ts @@ -1,4 +1,4 @@ -import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; +import type { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; diff --git a/migrations/1779742831642_principal-bond-positions.ts b/migrations/1779742831642_principal-bond-positions.ts index a4e1d5aba..ada052486 100644 --- a/migrations/1779742831642_principal-bond-positions.ts +++ b/migrations/1779742831642_principal-bond-positions.ts @@ -1,4 +1,4 @@ -import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; +import type { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined; diff --git a/migrations/1779745340752_bond-reward-distributions.ts b/migrations/1779745340752_bond-reward-distributions.ts index 72a7b8631..4f427b124 100644 --- a/migrations/1779745340752_bond-reward-distributions.ts +++ b/migrations/1779745340752_bond-reward-distributions.ts @@ -1,4 +1,4 @@ -import { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; +import type { ColumnDefinitions, MigrationBuilder } from 'node-pg-migrate'; export const shorthands: ColumnDefinitions | undefined = undefined;