From 061f4451ac7939c050f5c05c2d5595d7656bff77 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:41:34 -0600 Subject: [PATCH 1/6] btc lockup init --- .../1779487975550_bond-registrations.ts | 11 +++- package-lock.json | 8 +-- package.json | 2 +- .../schemas/v3/entities/bond-registrations.ts | 24 ++++++++- src/api/serializers/v3/bonds.ts | 16 +++++- src/datastore/common.ts | 25 ++++++++- src/datastore/pg-write-store.ts | 8 ++- src/datastore/v3/constants.ts | 3 +- src/datastore/v3/pg-store-v3.ts | 23 +++++++- src/datastore/v3/types.ts | 20 ++++++- tests/api/pox5/bonds.test.ts | 52 +++++++++++++++++-- 11 files changed, 172 insertions(+), 20 deletions(-) diff --git a/migrations/1779487975550_bond-registrations.ts b/migrations/1779487975550_bond-registrations.ts index 90b67ce58..124f7e523 100644 --- a/migrations/1779487975550_bond-registrations.ts +++ b/migrations/1779487975550_bond-registrations.ts @@ -91,10 +91,17 @@ export const up = (pgm: MigrationBuilder) => { type: 'integer', notNull: true, }, - is_l1_lock: { - type: 'boolean', + // How the BTC backing this registration was locked, as a `DbBondLockupType` + // smallint (0 = proven Bitcoin L1 lockup, 1 = sBTC lockup). + btc_lockup_type: { + type: 'smallint', notNull: true, }, + // The proven L1 lockup outputs (array of `{ txid, output_index }`) for an + // 'l1' lockup; null for an 'l2' (sBTC) lockup. + btc_lockup_txs: { + type: 'jsonb', + }, }); pgm.createIndex( diff --git a/package-lock.json b/package-lock.json index d064bef0b..2edeae7dd 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": "2.0.0-pox5.3", + "@stacks/codec": "2.0.0-pox5.4", "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", @@ -1699,9 +1699,9 @@ "license": "MIT" }, "node_modules/@stacks/codec": { - "version": "2.0.0-pox5.3", - "resolved": "https://registry.npmjs.org/@stacks/codec/-/codec-2.0.0-pox5.3.tgz", - "integrity": "sha512-uwZ5geY81huwEJu4MlDHL041Prrggcz2nR8Bm5iKOi8xbi52kCTwKpGl7s7kkFqqOHlfQYYqEfD+Civspj1org==", + "version": "2.0.0-pox5.4", + "resolved": "https://registry.npmjs.org/@stacks/codec/-/codec-2.0.0-pox5.4.tgz", + "integrity": "sha512-KD6ttE0CNrWpDIWt8Ytwm7ZWTxYYMVrit7WJ2WjEJakb21Qy3JSouvhaJFcqhZuWKiT+A8S9/aIrx830ykSdYQ==", "license": "GPL-3.0", "dependencies": { "@types/node": "^24.0.0", diff --git a/package.json b/package.json index 3884cea9a..4baf7378f 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@fastify/type-provider-typebox": "5.2.0", "@sinclair/typebox": "0.34.48", "@stacks/api-toolkit": "1.13.0", - "@stacks/codec": "2.0.0-pox5.3", + "@stacks/codec": "2.0.0-pox5.4", "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", diff --git a/src/api/schemas/v3/entities/bond-registrations.ts b/src/api/schemas/v3/entities/bond-registrations.ts index b54b32233..d2b96cc75 100644 --- a/src/api/schemas/v3/entities/bond-registrations.ts +++ b/src/api/schemas/v3/entities/bond-registrations.ts @@ -1,5 +1,27 @@ import { Static, Type } from '@sinclair/typebox'; +export const BondLockupTxSchema = Type.Object( + { + txid: Type.String({ + description: 'Reversed (big-endian) Bitcoin txid as a 0x-prefixed hex string', + }), + output_index: Type.String({ description: 'The output index of the proven L1 lockup' }), + }, + { title: 'BondLockupTx' } +); + +export const BondBtcLockupSchema = Type.Object( + { + type: Type.String({ + description: "'l1' for a proven Bitcoin L1 lockup, 'l2' for an sBTC lockup", + }), + txs: Type.Array(BondLockupTxSchema, { + description: 'The proven L1 lockup outputs; empty for an sBTC lockup', + }), + }, + { title: 'BondBtcLockup' } +); + export const BondRegistrationSchema = Type.Object({ bond_index: Type.Integer(), signer: Type.String(), @@ -9,6 +31,6 @@ export const BondRegistrationSchema = Type.Object({ first_reward_cycle: Type.Integer(), unlock_burn_height: Type.Integer(), unlock_cycle: Type.Integer(), - is_l1_lock: Type.Boolean(), + btc_lockup: BondBtcLockupSchema, }); export type BondRegistration = Static; diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts index 0a1cd2a90..465b7ac13 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -5,7 +5,7 @@ import { DbBondSummary, DbPrincipalBondPosition, } from '../../../datastore/v3/types.js'; -import { DbPrincipalBondPositionStatus } from '../../../datastore/common.js'; +import { DbBondLockupType, DbPrincipalBondPositionStatus } from '../../../datastore/common.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'; @@ -117,6 +117,15 @@ function getPrincipalBondPositionStatus( } } +function getBondLockupType(type: DbBondLockupType): 'l1' | 'l2' { + switch (type) { + case DbBondLockupType.L1: + return 'l1'; + case DbBondLockupType.L2: + return 'l2'; + } +} + export function serializeDbPrincipalBondPosition( position: DbPrincipalBondPosition ): PrincipalBondPosition { @@ -154,6 +163,9 @@ export function serializeDbBondRegistration(entry: DbBondRegistration): BondRegi 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, + btc_lockup: { + type: getBondLockupType(entry.btc_lockup_type), + txs: entry.btc_lockup_txs ?? [], + }, }; } diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 353c29751..52ac4592d 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1658,6 +1658,26 @@ export interface DbBondInsertValues extends DbTxLocation { early_unlock_admin: string; } +/** How the BTC backing a bond registration was locked. */ +export enum DbBondLockupType { + /** A proven Bitcoin L1 lockup. */ + L1 = 0, + /** An sBTC lockup. */ + L2 = 1, +} + +/** Maps a pox-5 `btc_lockup.type` string (`'l1'`/`'l2'`) to its enum value. */ +export function bondLockupTypeFromString(type: string): DbBondLockupType { + switch (type) { + case 'l1': + return DbBondLockupType.L1; + case 'l2': + return DbBondLockupType.L2; + default: + throw new Error(`Unknown bond lockup type: ${type}`); + } +} + export interface DbBondRegistrationInsertValues extends DbTxLocation { bond_index: number; signer: string; @@ -1667,7 +1687,10 @@ export interface DbBondRegistrationInsertValues extends DbTxLocation { first_reward_cycle: number; unlock_burn_height: number; unlock_cycle: number; - is_l1_lock: boolean; + /** `DbBondLockupType` stored as a smallint. */ + btc_lockup_type: DbBondLockupType; + /** JSON-encoded array of `{ txid, output_index }`, or null for an sBTC lockup. */ + btc_lockup_txs: string | null; } export interface DbBondAllowlistEntryInsertValues extends DbTxLocation { diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 4c7fc766f..e839ddbd2 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -71,6 +71,7 @@ import { DbBondAllowlistEntryInsertValues, DbPrincipalBondPositionInsertValues, DbPrincipalBondPositionStatus, + bondLockupTypeFromString, DbBondRewardCalculationInsertValues, DbBondRewardDistributionInsertValues, DbPrincipalBondRewardDistributionInsertValues, @@ -649,7 +650,12 @@ export class PgWriteStore extends PgStore { 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, + // Capture the BTC/sBTC lockup provenance from the event. `txs` lists the + // proven L1 outputs for an L1 lockup and is null for an sBTC lockup. + btc_lockup_type: bondLockupTypeFromString(event.data.btc_lockup.type), + btc_lockup_txs: event.data.btc_lockup.txs + ? JSON.stringify(event.data.btc_lockup.txs) + : null, }; await sql` INSERT INTO bond_registrations ${sql(bondRegistration)} diff --git a/src/datastore/v3/constants.ts b/src/datastore/v3/constants.ts index 6ad6766ac..1f5226011 100644 --- a/src/datastore/v3/constants.ts +++ b/src/datastore/v3/constants.ts @@ -142,7 +142,8 @@ export const BOND_REGISTRATION_COLUMNS = [ 'first_reward_cycle', 'unlock_burn_height', 'unlock_cycle', - 'is_l1_lock', + 'btc_lockup_type', + 'btc_lockup_txs', '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 2d800ce64..e9bcd2d09 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -2,6 +2,7 @@ import { BasePgStoreModule } from '@stacks/api-toolkit'; import { DbBond, DbBondAllowlistEntry, + DbBondLockupTx, DbBondRegistration, DbBondSummary, DbCursorPaginatedResult, @@ -39,6 +40,21 @@ import type { import { encodeTransactionCursor, resolveTransactionCursor } from './helpers.js'; import { DbEventTypeId } from '../common.js'; +/** + * Normalizes a `bond_registrations.btc_lockup_txs` jsonb value into a parsed + * array. The pg driver returns jsonb columns as raw strings here, so a string + * is JSON-parsed; an already-parsed array (or null) is returned as-is. + */ +function parseBondLockupTxs(value: unknown): DbBondLockupTx[] | null { + if (value == null) { + return null; + } + if (typeof value === 'string') { + return value.length > 0 ? (JSON.parse(value) as DbBondLockupTx[]) : null; + } + return value as DbBondLockupTx[]; +} + export class PgStoreV3 extends BasePgStoreModule { /** * Gets the summaries for all transactions. @@ -954,7 +970,7 @@ export class PgStoreV3 extends BasePgStoreModule { prev_cursor: prevCursor, current_cursor: firstResult ? encodeTransactionCursor(firstResult) : null, total: totalQuery[0]?.total ?? 0, - results, + results: results.map(r => ({ ...r, btc_lockup_txs: parseBondLockupTxs(r.btc_lockup_txs) })), }; }); } @@ -978,7 +994,10 @@ export class PgStoreV3 extends BasePgStoreModule { AND staker = ${args.principal} LIMIT 1 `; - return result[0] ?? null; + if (!result[0]) { + return null; + } + return { ...result[0], btc_lockup_txs: parseBondLockupTxs(result[0].btc_lockup_txs) }; }); } diff --git a/src/datastore/v3/types.ts b/src/datastore/v3/types.ts index 9d91caacf..6d5088ed1 100644 --- a/src/datastore/v3/types.ts +++ b/src/datastore/v3/types.ts @@ -1,4 +1,10 @@ -import { DbAssetEventTypeId, DbEventTypeId, DbTxStatus, DbTxTypeId } from '../common.js'; +import { + DbAssetEventTypeId, + DbBondLockupType, + DbEventTypeId, + DbTxStatus, + DbTxTypeId, +} from '../common.js'; export type DbCursorPaginatedResult = { limit: number; @@ -158,6 +164,13 @@ export interface DbBondAllowlistEntry { max_sats: string; } +export interface DbBondLockupTx { + /** Reversed (big-endian) txid as a `0x`-prefixed hex string. */ + txid: string; + /** String-quoted unsigned integer. */ + output_index: string; +} + export interface DbBondRegistration { bond_index: number; signer: string; @@ -167,7 +180,10 @@ export interface DbBondRegistration { first_reward_cycle: number; unlock_burn_height: number; unlock_cycle: number; - is_l1_lock: boolean; + /** `DbBondLockupType` stored as a smallint. */ + btc_lockup_type: DbBondLockupType; + /** Proven L1 lockup outputs (parsed from jsonb); null for an sBTC lockup. */ + btc_lockup_txs: DbBondLockupTx[] | null; } export interface DbPrincipalBondPosition { diff --git a/tests/api/pox5/bonds.test.ts b/tests/api/pox5/bonds.test.ts index c56ad2c95..5198d867d 100644 --- a/tests/api/pox5/bonds.test.ts +++ b/tests/api/pox5/bonds.test.ts @@ -84,7 +84,7 @@ interface BondRegistration { staker: string; amount_ustx: string; sats_total: string; - is_l1_lock: boolean; + btc_lockup: { type: string; txs: { txid: string; output_index: string }[] | null }; } interface CursorPaginated { total: number; @@ -149,6 +149,7 @@ describe('pox-5 bonds (simulated ingestion)', () => { unlock_burn_height: String(UNLOCK_BURN_HEIGHT), unlock_cycle: String(UNLOCK_CYCLE), is_l1_lock: false, + btc_lockup: { type: 'l2', txs: [] }, }, }) .build(); @@ -230,7 +231,8 @@ describe('pox-5 bonds (simulated ingestion)', () => { assert.equal(reg.signer, SIGNER); assert.equal(BigInt(reg.amount_ustx), AMOUNT_USTX); assert.equal(BigInt(reg.sats_total), SBTC_SATS); - assert.equal(reg.is_l1_lock, false); + // An sBTC ('l2') lockup carries no L1 outputs. + assert.deepEqual(reg.btc_lockup, { type: 'l2', txs: [] }); }); test('alice appears in GET .../registrations/:principal', async () => { @@ -240,7 +242,46 @@ describe('pox-5 bonds (simulated ingestion)', () => { assert.equal(reg.staker, ALICE); assert.equal(reg.bond_index, BOND_INDEX); assert.equal(BigInt(reg.amount_ustx), AMOUNT_USTX); - assert.equal(reg.is_l1_lock, false); + assert.deepEqual(reg.btc_lockup, { type: 'l2', txs: [] }); + }); + + test('an L1 lockup registration captures its proven Bitcoin outputs', async () => { + // Register bob with a proven Bitcoin L1 lockup (two outputs). + const l1Txs = [ + { txid: '0x' + 'ab'.repeat(32), output_index: '0' }, + { txid: '0x' + 'cd'.repeat(32), output_index: '3' }, + ]; + await db.update( + new TestBlockBuilder({ + block_height: 2, + block_hash: '0xb2', + index_block_hash: '0xb2', + parent_block_hash: '0xb1', + parent_index_block_hash: '0xb1', + }) + .addTx({ tx_id: '0x' + '33'.repeat(32) }) + .addTxPox5Event({ + name: Pox5EventName.RegisterForBond, + data: { + bond_index: String(BOND_INDEX), + signer: SIGNER, + staker: BOB, + amount_ustx: AMOUNT_USTX.toString(), + sats_total: BOB_MAX_SATS.toString(), + first_reward_cycle: String(FIRST_REWARD_CYCLE), + unlock_burn_height: String(UNLOCK_BURN_HEIGHT), + unlock_cycle: String(UNLOCK_CYCLE), + is_l1_lock: true, + btc_lockup: { type: 'l1', txs: l1Txs }, + }, + }) + .build() + ); + const reg = await getJson( + `/extended/v3/staking/bonds/${BOND_INDEX}/registrations/${BOB}` + ); + assert.equal(reg.staker, BOB); + assert.deepEqual(reg.btc_lockup, { type: 'l1', txs: l1Txs }); }); test("alice's position appears in GET .../principals/:principal/balances/staking", async () => { @@ -357,6 +398,7 @@ describe('pox-5 bonds lifecycle (multi-block)', () => { unlock_burn_height: String(UNLOCK_BURN_HEIGHT), unlock_cycle: String(UNLOCK_CYCLE), is_l1_lock: false, + btc_lockup: { type: 'l2', txs: [] }, }, }) .build() @@ -483,6 +525,7 @@ describe('pox-5 bonds reorg handling', () => { unlock_burn_height: String(UNLOCK_BURN_HEIGHT), unlock_cycle: String(UNLOCK_CYCLE), is_l1_lock: false, + btc_lockup: { type: 'l2', txs: [] }, }, }) .build(); @@ -611,6 +654,7 @@ describe('pox-5 bonds reorg handling', () => { unlock_burn_height: String(UNLOCK_BURN_HEIGHT), unlock_cycle: String(UNLOCK_CYCLE), is_l1_lock: false, + btc_lockup: { type: 'l2', txs: [] }, }, }) .build() @@ -723,6 +767,7 @@ describe('pox-5 bonds unstake / early-exit', () => { unlock_burn_height: String(UNLOCK_BURN_HEIGHT), unlock_cycle: String(UNLOCK_CYCLE), is_l1_lock: false, + btc_lockup: { type: 'l2', txs: [] }, }, }) .build() @@ -859,6 +904,7 @@ describe('pox-5 bonds reward accrual', () => { unlock_burn_height: String(UNLOCK_BURN_HEIGHT), unlock_cycle: String(UNLOCK_CYCLE), is_l1_lock: false, + btc_lockup: { type: 'l2', txs: [] }, }; } function distributionBlock(args: { From 2539e6f40f03900633bd1692597ea122cb0e3cd9 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:55:49 -0600 Subject: [PATCH 2/6] registration summaries --- src/api/routes/v3/staking-bonds.ts | 6 +++-- .../entities/bond-registration-summaries.ts | 26 +++++++++++++++++++ .../schemas/v3/entities/bond-registrations.ts | 15 ++++++++--- src/api/serializers/v3/bonds.ts | 14 +++++++++- tests/api/pox5/bonds.test.ts | 11 +++++--- 5 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 src/api/schemas/v3/entities/bond-registration-summaries.ts diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index a6576091f..67470bf0d 100644 --- a/src/api/routes/v3/staking-bonds.ts +++ b/src/api/routes/v3/staking-bonds.ts @@ -12,11 +12,13 @@ import { import { BondSchema, BondSummarySchema } from '../../schemas/v3/entities/bonds.js'; import { BondAllowlistSchema } from '../../schemas/v3/entities/bond-allowlist-entries.js'; import { BondRegistrationSchema } from '../../schemas/v3/entities/bond-registrations.js'; +import { BondRegistrationSummarySchema } from '../../schemas/v3/entities/bond-registration-summaries.js'; import { BondIndexSchema, PrincipalSchema } from '../../schemas/v3/entities/common.js'; import { serializeDbBond, serializeDbBondAllowlistEntry, serializeDbBondRegistration, + serializeDbBondRegistrationSummary, serializeDbBondSummary, } from '../../serializers/v3/bonds.js'; import { NotFoundError } from '../../../errors.js'; @@ -171,7 +173,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< querystring: CursorPaginationQuerystring(TransactionCursorSchema, ResourceType.Tx), response: { 200: CursorPaginatedResponse( - BondRegistrationSchema, + BondRegistrationSummarySchema, TransactionCursorSchema, ResourceType.Tx ), @@ -192,7 +194,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< previous: results.prev_cursor, current: results.current_cursor, }, - results: results.results.map(r => serializeDbBondRegistration(r)), + results: results.results.map(r => serializeDbBondRegistrationSummary(r)), }); } ); diff --git a/src/api/schemas/v3/entities/bond-registration-summaries.ts b/src/api/schemas/v3/entities/bond-registration-summaries.ts new file mode 100644 index 000000000..adbf7e0fb --- /dev/null +++ b/src/api/schemas/v3/entities/bond-registration-summaries.ts @@ -0,0 +1,26 @@ +import { Static, Type } from '@sinclair/typebox'; +import { bondRegistrationBaseProperties } from './bond-registrations.js'; + +/** The lockup type without the (potentially large) list of proven L1 outputs. */ +export const BondBtcLockupSummarySchema = Type.Object( + { + type: Type.String({ + description: "'l1' for a proven Bitcoin L1 lockup, 'l2' for an sBTC lockup", + }), + }, + { title: 'BondBtcLockupSummary' } +); + +/** + * A bond registration without the full list of L1 lockup transactions. Used by + * the bond registrations list endpoint; the proven L1 outputs are available on + * the per-principal registration endpoint. + */ +export const BondRegistrationSummarySchema = Type.Object( + { + ...bondRegistrationBaseProperties, + btc_lockup: BondBtcLockupSummarySchema, + }, + { title: 'BondRegistrationSummary' } +); +export type BondRegistrationSummary = Static; diff --git a/src/api/schemas/v3/entities/bond-registrations.ts b/src/api/schemas/v3/entities/bond-registrations.ts index d2b96cc75..209455c20 100644 --- a/src/api/schemas/v3/entities/bond-registrations.ts +++ b/src/api/schemas/v3/entities/bond-registrations.ts @@ -22,7 +22,8 @@ export const BondBtcLockupSchema = Type.Object( { title: 'BondBtcLockup' } ); -export const BondRegistrationSchema = Type.Object({ +/** Registration fields shared by the full and summary schemas. */ +export const bondRegistrationBaseProperties = { bond_index: Type.Integer(), signer: Type.String(), staker: Type.String(), @@ -31,6 +32,14 @@ export const BondRegistrationSchema = Type.Object({ first_reward_cycle: Type.Integer(), unlock_burn_height: Type.Integer(), unlock_cycle: Type.Integer(), - btc_lockup: BondBtcLockupSchema, -}); +}; + +/** A bond registration including the proven L1 lockup transactions. */ +export const BondRegistrationSchema = Type.Object( + { + ...bondRegistrationBaseProperties, + btc_lockup: BondBtcLockupSchema, + }, + { title: 'BondRegistration' } +); export type BondRegistration = Static; diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts index 465b7ac13..4796dcf68 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -10,6 +10,7 @@ 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'; +import { BondRegistrationSummary } from '../../schemas/v3/entities/bond-registration-summaries.js'; import { PrincipalBondPosition, PrincipalBondPositionStatus, @@ -153,7 +154,9 @@ export function serializeDbPrincipalBondPosition( }; } -export function serializeDbBondRegistration(entry: DbBondRegistration): BondRegistration { +export function serializeDbBondRegistrationSummary( + entry: DbBondRegistration +): BondRegistrationSummary { return { bond_index: entry.bond_index, signer: entry.signer, @@ -163,6 +166,15 @@ export function serializeDbBondRegistration(entry: DbBondRegistration): BondRegi first_reward_cycle: entry.first_reward_cycle, unlock_burn_height: entry.unlock_burn_height, unlock_cycle: entry.unlock_cycle, + btc_lockup: { + type: getBondLockupType(entry.btc_lockup_type), + }, + }; +} + +export function serializeDbBondRegistration(entry: DbBondRegistration): BondRegistration { + return { + ...serializeDbBondRegistrationSummary(entry), btc_lockup: { type: getBondLockupType(entry.btc_lockup_type), txs: entry.btc_lockup_txs ?? [], diff --git a/tests/api/pox5/bonds.test.ts b/tests/api/pox5/bonds.test.ts index 5198d867d..c3e92a007 100644 --- a/tests/api/pox5/bonds.test.ts +++ b/tests/api/pox5/bonds.test.ts @@ -78,12 +78,15 @@ interface BondAllowlistEntry { staker: string; max_sats: string; } -interface BondRegistration { +interface BondRegistrationSummary { bond_index: number; signer: string; staker: string; amount_ustx: string; sats_total: string; + btc_lockup: { type: string }; +} +interface BondRegistration extends Omit { btc_lockup: { type: string; txs: { txid: string; output_index: string }[] | null }; } interface CursorPaginated { @@ -222,7 +225,7 @@ describe('pox-5 bonds (simulated ingestion)', () => { }); test("alice's registration appears in GET .../registrations", async () => { - const list = await getJson>( + const list = await getJson>( `/extended/v3/staking/bonds/${BOND_INDEX}/registrations?limit=50` ); const reg = list.results.find(r => r.staker === ALICE); @@ -231,8 +234,8 @@ describe('pox-5 bonds (simulated ingestion)', () => { assert.equal(reg.signer, SIGNER); assert.equal(BigInt(reg.amount_ustx), AMOUNT_USTX); assert.equal(BigInt(reg.sats_total), SBTC_SATS); - // An sBTC ('l2') lockup carries no L1 outputs. - assert.deepEqual(reg.btc_lockup, { type: 'l2', txs: [] }); + // The list endpoint returns the lockup summary — type only, no L1 tx list. + assert.deepEqual(reg.btc_lockup, { type: 'l2' }); }); test('alice appears in GET .../registrations/:principal', async () => { From 4917e4a8ca7a29b46cb78166d899d9dac8cac7d3 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:43:08 -0600 Subject: [PATCH 3/6] adjust schemas --- .../entities/bond-registration-summaries.ts | 29 ++++----- .../schemas/v3/entities/bond-registrations.ts | 63 ++++++++----------- 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/src/api/schemas/v3/entities/bond-registration-summaries.ts b/src/api/schemas/v3/entities/bond-registration-summaries.ts index adbf7e0fb..aec027668 100644 --- a/src/api/schemas/v3/entities/bond-registration-summaries.ts +++ b/src/api/schemas/v3/entities/bond-registration-summaries.ts @@ -1,26 +1,21 @@ import { Static, Type } from '@sinclair/typebox'; -import { bondRegistrationBaseProperties } from './bond-registrations.js'; +import { AmountSchema } from './common.js'; -/** The lockup type without the (potentially large) list of proven L1 outputs. */ -export const BondBtcLockupSummarySchema = Type.Object( - { - type: Type.String({ - description: "'l1' for a proven Bitcoin L1 lockup, 'l2' for an sBTC lockup", - }), - }, - { title: 'BondBtcLockupSummary' } -); +export const BondRegistrationTypeSchema = Type.Union([Type.Literal('l1'), Type.Literal('l2')]); +export type BondRegistrationType = Static; /** * A bond registration without the full list of L1 lockup transactions. Used by * the bond registrations list endpoint; the proven L1 outputs are available on * the per-principal registration endpoint. */ -export const BondRegistrationSummarySchema = Type.Object( - { - ...bondRegistrationBaseProperties, - btc_lockup: BondBtcLockupSummarySchema, - }, - { title: 'BondRegistrationSummary' } -); +export const BondRegistrationSummarySchema = Type.Object({ + staker: Type.String(), + signer: Type.String(), + type: BondRegistrationTypeSchema, + balances: Type.Object({ + btc: AmountSchema, + stx: AmountSchema, + }), +}); export type BondRegistrationSummary = Static; diff --git a/src/api/schemas/v3/entities/bond-registrations.ts b/src/api/schemas/v3/entities/bond-registrations.ts index 209455c20..9f334dd1a 100644 --- a/src/api/schemas/v3/entities/bond-registrations.ts +++ b/src/api/schemas/v3/entities/bond-registrations.ts @@ -1,45 +1,36 @@ import { Static, Type } from '@sinclair/typebox'; +import { BondRegistrationSummarySchema } from './bond-registration-summaries.js'; +import { TransactionIdSchema } from './common.js'; -export const BondLockupTxSchema = Type.Object( - { - txid: Type.String({ - description: 'Reversed (big-endian) Bitcoin txid as a 0x-prefixed hex string', - }), - output_index: Type.String({ description: 'The output index of the proven L1 lockup' }), - }, - { title: 'BondLockupTx' } -); +export const BondRegistrationBtcLockupTransactionSchema = Type.Object({ + tx_id: TransactionIdSchema, + output_index: Type.Integer({ description: 'The output index of the proven L1 lockup' }), +}); +export type BondRegistrationBtcLockupTransaction = Static< + typeof BondRegistrationBtcLockupTransactionSchema +>; -export const BondBtcLockupSchema = Type.Object( - { - type: Type.String({ - description: "'l1' for a proven Bitcoin L1 lockup, 'l2' for an sBTC lockup", +export const BondRegistrationBtcLockupSchema = Type.Object({ + l1_lockup: Type.Object({ + transactions: Type.Array(BondRegistrationBtcLockupTransactionSchema, { + description: 'The proven L1 lockup transactions', }), - txs: Type.Array(BondLockupTxSchema, { - description: 'The proven L1 lockup outputs; empty for an sBTC lockup', - }), - }, - { title: 'BondBtcLockup' } -); + }), +}); +export type BondRegistrationBtcLockup = Static; -/** Registration fields shared by the full and summary schemas. */ -export const bondRegistrationBaseProperties = { - bond_index: Type.Integer(), - 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(), -}; +export const BondRegistrationSbtcLockupSchema = Type.Object({ + l2_lockup: Type.Object({ + tx_id: TransactionIdSchema, + }), +}); +export type BondRegistrationSbtcLockup = Static; -/** A bond registration including the proven L1 lockup transactions. */ -export const BondRegistrationSchema = Type.Object( - { - ...bondRegistrationBaseProperties, - btc_lockup: BondBtcLockupSchema, - }, +export const BondRegistrationSchema = Type.Composite( + [ + BondRegistrationSummarySchema, + Type.Union([BondRegistrationBtcLockupSchema, BondRegistrationSbtcLockupSchema]), + ], { title: 'BondRegistration' } ); export type BondRegistration = Static; From 2fa6fa2c38afbcb0a1bbabe661f29d11d5db9d22 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:57:29 -0600 Subject: [PATCH 4/6] serialize registrations --- .../schemas/v3/entities/bond-registrations.ts | 35 ++++++++------- src/api/serializers/v3/bonds.ts | 43 ++++++++++++------- src/datastore/v3/constants.ts | 14 +++--- src/datastore/v3/helpers.ts | 16 +++++++ src/datastore/v3/pg-store-v3.ts | 28 ++++-------- src/datastore/v3/types.ts | 12 +++--- 6 files changed, 82 insertions(+), 66 deletions(-) diff --git a/src/api/schemas/v3/entities/bond-registrations.ts b/src/api/schemas/v3/entities/bond-registrations.ts index 9f334dd1a..b81d63c37 100644 --- a/src/api/schemas/v3/entities/bond-registrations.ts +++ b/src/api/schemas/v3/entities/bond-registrations.ts @@ -10,27 +10,30 @@ export type BondRegistrationBtcLockupTransaction = Static< typeof BondRegistrationBtcLockupTransactionSchema >; -export const BondRegistrationBtcLockupSchema = Type.Object({ - l1_lockup: Type.Object({ - transactions: Type.Array(BondRegistrationBtcLockupTransactionSchema, { - description: 'The proven L1 lockup transactions', +export const BondRegistrationBtcLockupSchema = Type.Composite([ + BondRegistrationSummarySchema, + Type.Object({ + l1_lockup: Type.Object({ + transactions: Type.Array(BondRegistrationBtcLockupTransactionSchema, { + description: 'The proven L1 lockup transactions', + }), }), }), -}); +]); export type BondRegistrationBtcLockup = Static; -export const BondRegistrationSbtcLockupSchema = Type.Object({ - l2_lockup: Type.Object({ - tx_id: TransactionIdSchema, +export const BondRegistrationSbtcLockupSchema = Type.Composite([ + BondRegistrationSummarySchema, + Type.Object({ + l2_lockup: Type.Object({ + tx_id: TransactionIdSchema, + }), }), -}); +]); export type BondRegistrationSbtcLockup = Static; -export const BondRegistrationSchema = Type.Composite( - [ - BondRegistrationSummarySchema, - Type.Union([BondRegistrationBtcLockupSchema, BondRegistrationSbtcLockupSchema]), - ], - { title: 'BondRegistration' } -); +export const BondRegistrationSchema = Type.Union([ + BondRegistrationBtcLockupSchema, + BondRegistrationSbtcLockupSchema, +]); export type BondRegistration = Static; diff --git a/src/api/serializers/v3/bonds.ts b/src/api/serializers/v3/bonds.ts index 4796dcf68..1d17fc4f9 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -2,6 +2,7 @@ import { DbBond, DbBondAllowlistEntry, DbBondRegistration, + DbBondRegistrationSummary, DbBondSummary, DbPrincipalBondPosition, } from '../../../datastore/v3/types.js'; @@ -155,29 +156,39 @@ export function serializeDbPrincipalBondPosition( } export function serializeDbBondRegistrationSummary( - entry: DbBondRegistration + entry: DbBondRegistrationSummary ): BondRegistrationSummary { 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, - btc_lockup: { - type: getBondLockupType(entry.btc_lockup_type), + type: getBondLockupType(entry.btc_lockup_type), + balances: { + btc: entry.sats_total, + stx: entry.amount_ustx, }, }; } export function serializeDbBondRegistration(entry: DbBondRegistration): BondRegistration { - return { - ...serializeDbBondRegistrationSummary(entry), - btc_lockup: { - type: getBondLockupType(entry.btc_lockup_type), - txs: entry.btc_lockup_txs ?? [], - }, - }; + const summary = serializeDbBondRegistrationSummary(entry); + switch (summary.type) { + case 'l1': + return { + ...summary, + l1_lockup: { + transactions: + entry.btc_lockup_txs?.map(tx => ({ + tx_id: tx.txid, + output_index: parseInt(tx.output_index), + })) ?? [], + }, + }; + case 'l2': + return { + ...summary, + l2_lockup: { + tx_id: entry.tx_id, + }, + }; + } } diff --git a/src/datastore/v3/constants.ts b/src/datastore/v3/constants.ts index 1f5226011..f0df14f08 100644 --- a/src/datastore/v3/constants.ts +++ b/src/datastore/v3/constants.ts @@ -133,20 +133,18 @@ export const BOND_ALLOWLIST_ENTRY_COLUMNS = [ 'tx_index', ]; -export const BOND_REGISTRATION_COLUMNS = [ - 'bond_index', +export const BOND_REGISTRATION_SUMMARY_COLUMNS = [ 'signer', 'staker', 'amount_ustx', 'sats_total', - 'first_reward_cycle', - 'unlock_burn_height', - 'unlock_cycle', 'btc_lockup_type', +]; + +export const BOND_REGISTRATION_COLUMNS = [ + ...BOND_REGISTRATION_SUMMARY_COLUMNS, 'btc_lockup_txs', - 'block_height', - 'microblock_sequence', - 'tx_index', + 'tx_id', ]; export const PRINCIPAL_BOND_POSITION_COLUMNS = [ diff --git a/src/datastore/v3/helpers.ts b/src/datastore/v3/helpers.ts index faff452d0..93361332b 100644 --- a/src/datastore/v3/helpers.ts +++ b/src/datastore/v3/helpers.ts @@ -1,5 +1,6 @@ import { TransactionCursor } from '../../api/schemas/v3/cursors.js'; import { I32_MAX } from '../../helpers.js'; +import { DbBondLockupTx } from './types.js'; const MAX_TX_INDEX = 0x7fff; @@ -40,3 +41,18 @@ export const resolveTransactionCursor = async ( export const encodeTransactionCursor = (tx: TransactionCursorRow): TransactionCursor => `${tx.block_height}:${tx.microblock_sequence}:${tx.tx_index}`; + +/** + * Normalizes a `bond_registrations.btc_lockup_txs` jsonb value into a parsed + * array. The pg driver returns jsonb columns as raw strings here, so a string + * is JSON-parsed; an already-parsed array (or null) is returned as-is. + */ +export function parseBondLockupTxs(value: unknown): DbBondLockupTx[] | null { + if (value == null) { + return null; + } + if (typeof value === 'string') { + return value.length > 0 ? (JSON.parse(value) as DbBondLockupTx[]) : null; + } + return value as DbBondLockupTx[]; +} diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index e9bcd2d09..58a116cf5 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -2,8 +2,8 @@ import { BasePgStoreModule } from '@stacks/api-toolkit'; import { DbBond, DbBondAllowlistEntry, - DbBondLockupTx, DbBondRegistration, + DbBondRegistrationSummary, DbBondSummary, DbCursorPaginatedResult, DbMempoolTransaction, @@ -19,6 +19,7 @@ import { BOND_ALLOWLIST_ENTRY_COLUMNS, BOND_COLUMNS, BOND_REGISTRATION_COLUMNS, + BOND_REGISTRATION_SUMMARY_COLUMNS, BOND_SUMMARY_COLUMNS, MEMPOOL_TX_COLUMNS, MEMPOOL_TX_SUMMARY_COLUMNS, @@ -37,24 +38,13 @@ import type { TransactionCursor, TransactionEventCursor, } from '../../api/schemas/v3/cursors.js'; -import { encodeTransactionCursor, resolveTransactionCursor } from './helpers.js'; +import { + encodeTransactionCursor, + parseBondLockupTxs, + resolveTransactionCursor, +} from './helpers.js'; import { DbEventTypeId } from '../common.js'; -/** - * Normalizes a `bond_registrations.btc_lockup_txs` jsonb value into a parsed - * array. The pg driver returns jsonb columns as raw strings here, so a string - * is JSON-parsed; an already-parsed array (or null) is returned as-is. - */ -function parseBondLockupTxs(value: unknown): DbBondLockupTx[] | null { - if (value == null) { - return null; - } - if (typeof value === 'string') { - return value.length > 0 ? (JSON.parse(value) as DbBondLockupTx[]) : null; - } - return value as DbBondLockupTx[]; -} - export class PgStoreV3 extends BasePgStoreModule { /** * Gets the summaries for all transactions. @@ -891,7 +881,7 @@ export class PgStoreV3 extends BasePgStoreModule { bondIndex: number; limit: number; cursor?: TransactionCursor; - }): Promise> { + }): Promise> { return await this.sqlTransaction(async sql => { const limit = args.limit; let cursorFilter = sql``; @@ -926,7 +916,7 @@ export class PgStoreV3 extends BasePgStoreModule { `; const resultQuery = await sql<(DbBondRegistration & DbTransactionCursor)[]>` - SELECT ${sql(BOND_REGISTRATION_COLUMNS)} + SELECT ${sql(BOND_REGISTRATION_SUMMARY_COLUMNS)} FROM bond_registrations WHERE canonical = true AND microblock_canonical = true diff --git a/src/datastore/v3/types.ts b/src/datastore/v3/types.ts index 6d5088ed1..afde33bbc 100644 --- a/src/datastore/v3/types.ts +++ b/src/datastore/v3/types.ts @@ -171,18 +171,16 @@ export interface DbBondLockupTx { output_index: string; } -export interface DbBondRegistration { - bond_index: number; +export interface DbBondRegistrationSummary { signer: string; staker: string; amount_ustx: string; sats_total: string; - first_reward_cycle: number; - unlock_burn_height: number; - unlock_cycle: number; - /** `DbBondLockupType` stored as a smallint. */ btc_lockup_type: DbBondLockupType; - /** Proven L1 lockup outputs (parsed from jsonb); null for an sBTC lockup. */ +} + +export interface DbBondRegistration extends DbBondRegistrationSummary { + tx_id: string; btc_lockup_txs: DbBondLockupTx[] | null; } From f1e29752541121f344a57c82801ba052ccc75909 Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:07:02 -0600 Subject: [PATCH 5/6] fix tests --- src/api/routes/v3/staking-bonds.ts | 2 +- src/datastore/v3/pg-store-v3.ts | 8 ++-- tests/api/pox5/bonds.test.ts | 66 +++++++++++++++++++----------- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/api/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index 67470bf0d..c78e9b2cb 100644 --- a/src/api/routes/v3/staking-bonds.ts +++ b/src/api/routes/v3/staking-bonds.ts @@ -181,7 +181,7 @@ export const StakingBondsRoutes: FastifyPluginAsync< }, }, async (req, reply) => { - const results = await fastify.db.v3.getBondRegistrations({ + const results = await fastify.db.v3.getBondRegistrationSummaries({ bondIndex: req.params.bond_index, limit: req.query.limit ?? getPagingQueryLimit(ResourceType.Tx), cursor: req.query.cursor, diff --git a/src/datastore/v3/pg-store-v3.ts b/src/datastore/v3/pg-store-v3.ts index 58a116cf5..673b96c6e 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -877,7 +877,7 @@ export class PgStoreV3 extends BasePgStoreModule { * @param args - The arguments for the query. * @returns The registrations for a bond. */ - async getBondRegistrations(args: { + async getBondRegistrationSummaries(args: { bondIndex: number; limit: number; cursor?: TransactionCursor; @@ -915,8 +915,8 @@ export class PgStoreV3 extends BasePgStoreModule { LIMIT 1 `; - const resultQuery = await sql<(DbBondRegistration & DbTransactionCursor)[]>` - SELECT ${sql(BOND_REGISTRATION_SUMMARY_COLUMNS)} + const resultQuery = await sql<(DbBondRegistrationSummary & DbTransactionCursor)[]>` + SELECT ${sql(BOND_REGISTRATION_SUMMARY_COLUMNS)}, block_height, microblock_sequence, tx_index FROM bond_registrations WHERE canonical = true AND microblock_canonical = true @@ -960,7 +960,7 @@ export class PgStoreV3 extends BasePgStoreModule { prev_cursor: prevCursor, current_cursor: firstResult ? encodeTransactionCursor(firstResult) : null, total: totalQuery[0]?.total ?? 0, - results: results.map(r => ({ ...r, btc_lockup_txs: parseBondLockupTxs(r.btc_lockup_txs) })), + results, }; }); } diff --git a/tests/api/pox5/bonds.test.ts b/tests/api/pox5/bonds.test.ts index c3e92a007..616f4315b 100644 --- a/tests/api/pox5/bonds.test.ts +++ b/tests/api/pox5/bonds.test.ts @@ -79,15 +79,14 @@ interface BondAllowlistEntry { max_sats: string; } interface BondRegistrationSummary { - bond_index: number; signer: string; staker: string; - amount_ustx: string; - sats_total: string; - btc_lockup: { type: string }; + type: 'l1' | 'l2'; + balances: { btc: string; stx: string }; } -interface BondRegistration extends Omit { - btc_lockup: { type: string; txs: { txid: string; output_index: string }[] | null }; +interface BondRegistration extends BondRegistrationSummary { + l1_lockup?: { transactions: { tx_id: string; output_index: number }[] }; + l2_lockup?: { tx_id: string }; } interface CursorPaginated { total: number; @@ -230,12 +229,13 @@ describe('pox-5 bonds (simulated ingestion)', () => { ); const reg = list.results.find(r => r.staker === ALICE); assert.ok(reg, 'alice present in registrations'); - assert.equal(reg.bond_index, BOND_INDEX); - assert.equal(reg.signer, SIGNER); - assert.equal(BigInt(reg.amount_ustx), AMOUNT_USTX); - assert.equal(BigInt(reg.sats_total), SBTC_SATS); - // The list endpoint returns the lockup summary — type only, no L1 tx list. - assert.deepEqual(reg.btc_lockup, { type: 'l2' }); + // The list endpoint returns the lockup summary — no per-lockup tx details. + assert.deepEqual(reg, { + signer: SIGNER, + staker: ALICE, + type: 'l2', + balances: { btc: SBTC_SATS.toString(), stx: AMOUNT_USTX.toString() }, + }); }); test('alice appears in GET .../registrations/:principal', async () => { @@ -243,17 +243,20 @@ describe('pox-5 bonds (simulated ingestion)', () => { `/extended/v3/staking/bonds/${BOND_INDEX}/registrations/${ALICE}` ); assert.equal(reg.staker, ALICE); - assert.equal(reg.bond_index, BOND_INDEX); - assert.equal(BigInt(reg.amount_ustx), AMOUNT_USTX); - assert.deepEqual(reg.btc_lockup, { type: 'l2', txs: [] }); + assert.equal(reg.signer, SIGNER); + assert.equal(reg.type, 'l2'); + assert.equal(BigInt(reg.balances.stx), AMOUNT_USTX); + assert.equal(BigInt(reg.balances.btc), SBTC_SATS); + // An sBTC ('l2') lockup links to its registration tx, not L1 outputs. + assert.equal(reg.l1_lockup, undefined); + assert.ok(reg.l2_lockup, 'l2_lockup present'); + assert.equal(normalizeTxId(reg.l2_lockup.tx_id), normalizeTxId(REGISTER_TX_ID)); }); test('an L1 lockup registration captures its proven Bitcoin outputs', async () => { // Register bob with a proven Bitcoin L1 lockup (two outputs). - const l1Txs = [ - { txid: '0x' + 'ab'.repeat(32), output_index: '0' }, - { txid: '0x' + 'cd'.repeat(32), output_index: '3' }, - ]; + const txid1 = '0x' + 'ab'.repeat(32); + const txid2 = '0x' + 'cd'.repeat(32); await db.update( new TestBlockBuilder({ block_height: 2, @@ -275,7 +278,13 @@ describe('pox-5 bonds (simulated ingestion)', () => { unlock_burn_height: String(UNLOCK_BURN_HEIGHT), unlock_cycle: String(UNLOCK_CYCLE), is_l1_lock: true, - btc_lockup: { type: 'l1', txs: l1Txs }, + btc_lockup: { + type: 'l1', + txs: [ + { txid: txid1, output_index: '0' }, + { txid: txid2, output_index: '3' }, + ], + }, }, }) .build() @@ -284,7 +293,14 @@ describe('pox-5 bonds (simulated ingestion)', () => { `/extended/v3/staking/bonds/${BOND_INDEX}/registrations/${BOB}` ); assert.equal(reg.staker, BOB); - assert.deepEqual(reg.btc_lockup, { type: 'l1', txs: l1Txs }); + assert.equal(reg.type, 'l1'); + assert.equal(reg.l2_lockup, undefined); + assert.deepEqual(reg.l1_lockup, { + transactions: [ + { tx_id: txid1, output_index: 0 }, + { tx_id: txid2, output_index: 3 }, + ], + }); }); test("alice's position appears in GET .../principals/:principal/balances/staking", async () => { @@ -409,8 +425,8 @@ describe('pox-5 bonds lifecycle (multi-block)', () => { const reg = await getRegistration(); assert.equal(reg.signer, SIGNER); - assert.equal(BigInt(reg.amount_ustx), AMOUNT_USTX); - assert.equal(BigInt(reg.sats_total), SBTC_SATS); + assert.equal(BigInt(reg.balances.stx), AMOUNT_USTX); + assert.equal(BigInt(reg.balances.btc), SBTC_SATS); // --- Block 3: alice update-bond-registration --- await db.update( @@ -438,8 +454,8 @@ describe('pox-5 bonds lifecycle (multi-block)', () => { const updated = await getRegistration(); assert.equal(updated.signer, NEW_SIGNER, 'signer updated'); - assert.equal(BigInt(updated.amount_ustx), UPDATED_AMOUNT_USTX, 'amount_ustx updated'); - assert.equal(BigInt(updated.sats_total), UPDATED_SATS, 'sats_total updated'); + assert.equal(BigInt(updated.balances.stx), UPDATED_AMOUNT_USTX, 'amount_ustx updated'); + assert.equal(BigInt(updated.balances.btc), UPDATED_SATS, 'sats_total updated'); // Still a single registration for alice (update, not a new row). const regs = await getJson>( `/extended/v3/staking/bonds/${BOND_INDEX}/registrations?limit=50` From 091053e9eb81303f1de6879ac8b3a0ada474191a Mon Sep 17 00:00:00 2001 From: Rafa Cardenas <253999660+rafa-stacks@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:15:19 -0600 Subject: [PATCH 6/6] update pr template --- .github/PULL_REQUEST_TEMPLATE.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b8a546157..d35d7df41 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -54,10 +54,3 @@ Provide context on how tests should be performed. 3. Briefly mention affected code paths 4. List other affected projects if possible 5. Things to watch out for when testing - -## Checklist -- [ ] Code is commented where needed -- [ ] Unit test coverage for new or modified code paths -- [ ] `npm run test` passes -- [ ] Changelog is updated -- [ ] Tag 1 of @rafaelcr or @zone117x for review