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 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/routes/v3/staking-bonds.ts b/src/api/routes/v3/staking-bonds.ts index a6576091f..c78e9b2cb 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 ), @@ -179,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, @@ -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..aec027668 --- /dev/null +++ b/src/api/schemas/v3/entities/bond-registration-summaries.ts @@ -0,0 +1,21 @@ +import { Static, Type } from '@sinclair/typebox'; +import { AmountSchema } from './common.js'; + +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({ + 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 b54b32233..b81d63c37 100644 --- a/src/api/schemas/v3/entities/bond-registrations.ts +++ b/src/api/schemas/v3/entities/bond-registrations.ts @@ -1,14 +1,39 @@ import { Static, Type } from '@sinclair/typebox'; +import { BondRegistrationSummarySchema } from './bond-registration-summaries.js'; +import { TransactionIdSchema } from './common.js'; -export const BondRegistrationSchema = Type.Object({ - 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(), - is_l1_lock: Type.Boolean(), +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 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.Composite([ + BondRegistrationSummarySchema, + Type.Object({ + l2_lockup: Type.Object({ + tx_id: TransactionIdSchema, + }), + }), +]); +export type BondRegistrationSbtcLockup = Static; + +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 0a1cd2a90..1d17fc4f9 100644 --- a/src/api/serializers/v3/bonds.ts +++ b/src/api/serializers/v3/bonds.ts @@ -2,14 +2,16 @@ import { DbBond, DbBondAllowlistEntry, DbBondRegistration, + DbBondRegistrationSummary, 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'; import { BondRegistration } from '../../schemas/v3/entities/bond-registrations.js'; +import { BondRegistrationSummary } from '../../schemas/v3/entities/bond-registration-summaries.js'; import { PrincipalBondPosition, PrincipalBondPositionStatus, @@ -117,6 +119,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 { @@ -144,16 +155,40 @@ export function serializeDbPrincipalBondPosition( }; } -export function serializeDbBondRegistration(entry: DbBondRegistration): BondRegistration { +export function serializeDbBondRegistrationSummary( + 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, - is_l1_lock: entry.is_l1_lock, + type: getBondLockupType(entry.btc_lockup_type), + balances: { + btc: entry.sats_total, + stx: entry.amount_ustx, + }, }; } + +export function serializeDbBondRegistration(entry: DbBondRegistration): BondRegistration { + 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/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..f0df14f08 100644 --- a/src/datastore/v3/constants.ts +++ b/src/datastore/v3/constants.ts @@ -133,19 +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', - 'is_l1_lock', - 'block_height', - 'microblock_sequence', - 'tx_index', + 'btc_lockup_type', +]; + +export const BOND_REGISTRATION_COLUMNS = [ + ...BOND_REGISTRATION_SUMMARY_COLUMNS, + 'btc_lockup_txs', + '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 2d800ce64..673b96c6e 100644 --- a/src/datastore/v3/pg-store-v3.ts +++ b/src/datastore/v3/pg-store-v3.ts @@ -3,6 +3,7 @@ import { DbBond, DbBondAllowlistEntry, DbBondRegistration, + DbBondRegistrationSummary, DbBondSummary, DbCursorPaginatedResult, DbMempoolTransaction, @@ -18,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, @@ -36,7 +38,11 @@ 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'; export class PgStoreV3 extends BasePgStoreModule { @@ -871,11 +877,11 @@ 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; - }): Promise> { + }): Promise> { return await this.sqlTransaction(async sql => { const limit = args.limit; let cursorFilter = sql``; @@ -909,8 +915,8 @@ export class PgStoreV3 extends BasePgStoreModule { LIMIT 1 `; - const resultQuery = await sql<(DbBondRegistration & DbTransactionCursor)[]>` - SELECT ${sql(BOND_REGISTRATION_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 @@ -978,7 +984,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..afde33bbc 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,16 +164,24 @@ export interface DbBondAllowlistEntry { max_sats: string; } -export interface DbBondRegistration { - bond_index: number; +export interface DbBondLockupTx { + /** Reversed (big-endian) txid as a `0x`-prefixed hex string. */ + txid: string; + /** String-quoted unsigned integer. */ + output_index: string; +} + +export interface DbBondRegistrationSummary { 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; + btc_lockup_type: DbBondLockupType; +} + +export interface DbBondRegistration extends DbBondRegistrationSummary { + tx_id: string; + 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..616f4315b 100644 --- a/tests/api/pox5/bonds.test.ts +++ b/tests/api/pox5/bonds.test.ts @@ -78,13 +78,15 @@ interface BondAllowlistEntry { staker: string; max_sats: string; } -interface BondRegistration { - bond_index: number; +interface BondRegistrationSummary { signer: string; staker: string; - amount_ustx: string; - sats_total: string; - is_l1_lock: boolean; + type: 'l1' | 'l2'; + balances: { btc: string; stx: string }; +} +interface BondRegistration extends BondRegistrationSummary { + l1_lockup?: { transactions: { tx_id: string; output_index: number }[] }; + l2_lockup?: { tx_id: string }; } interface CursorPaginated { total: number; @@ -149,6 +151,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(); @@ -221,16 +224,18 @@ 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); 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); - assert.equal(reg.is_l1_lock, false); + // 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 () => { @@ -238,9 +243,64 @@ 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.equal(reg.is_l1_lock, false); + 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 txid1 = '0x' + 'ab'.repeat(32); + const txid2 = '0x' + 'cd'.repeat(32); + 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: [ + { txid: txid1, output_index: '0' }, + { txid: txid2, output_index: '3' }, + ], + }, + }, + }) + .build() + ); + const reg = await getJson( + `/extended/v3/staking/bonds/${BOND_INDEX}/registrations/${BOB}` + ); + assert.equal(reg.staker, BOB); + 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 () => { @@ -357,6 +417,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() @@ -364,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( @@ -393,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` @@ -483,6 +544,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 +673,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 +786,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 +923,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: {