Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 9 additions & 2 deletions migrations/1779487975550_bond-registrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions src/api/routes/v3/staking-bonds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -171,15 +173,15 @@ export const StakingBondsRoutes: FastifyPluginAsync<
querystring: CursorPaginationQuerystring(TransactionCursorSchema, ResourceType.Tx),
response: {
200: CursorPaginatedResponse(
BondRegistrationSchema,
BondRegistrationSummarySchema,
TransactionCursorSchema,
ResourceType.Tx
),
},
},
},
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,
Expand All @@ -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)),
});
}
);
Expand Down
21 changes: 21 additions & 0 deletions src/api/schemas/v3/entities/bond-registration-summaries.ts
Original file line number Diff line number Diff line change
@@ -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<typeof BondRegistrationTypeSchema>;

/**
* 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<typeof BondRegistrationSummarySchema>;
45 changes: 35 additions & 10 deletions src/api/schemas/v3/entities/bond-registrations.ts
Original file line number Diff line number Diff line change
@@ -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<typeof BondRegistrationBtcLockupSchema>;

export const BondRegistrationSbtcLockupSchema = Type.Composite([
BondRegistrationSummarySchema,
Type.Object({
l2_lockup: Type.Object({
tx_id: TransactionIdSchema,
}),
}),
]);
export type BondRegistrationSbtcLockup = Static<typeof BondRegistrationSbtcLockupSchema>;

export const BondRegistrationSchema = Type.Union([
BondRegistrationBtcLockupSchema,
BondRegistrationSbtcLockupSchema,
]);
export type BondRegistration = Static<typeof BondRegistrationSchema>;
53 changes: 44 additions & 9 deletions src/api/serializers/v3/bonds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
},
};
}
}
25 changes: 24 additions & 1 deletion src/datastore/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@
canonical: boolean;

microblock_canonical: boolean;
// TODO(mb): should probably be (number | null) rather than -1 for batched tx

Check warning on line 212 in src/datastore/common.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO(mb): should probably be (number |...'
microblock_sequence: number;
// TODO(mb): should probably be (string | null) rather than empty string for batched tx

Check warning on line 214 in src/datastore/common.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO(mb): should probably be (string |...'
microblock_hash: string;

post_conditions: string;
Expand Down Expand Up @@ -1658,6 +1658,26 @@
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;
Expand All @@ -1667,7 +1687,10 @@
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 {
Expand Down
8 changes: 7 additions & 1 deletion src/datastore/pg-write-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
DbBondAllowlistEntryInsertValues,
DbPrincipalBondPositionInsertValues,
DbPrincipalBondPositionStatus,
bondLockupTypeFromString,
DbBondRewardCalculationInsertValues,
DbBondRewardDistributionInsertValues,
DbPrincipalBondRewardDistributionInsertValues,
Expand Down Expand Up @@ -581,7 +582,7 @@
break;
case Pox5EventName.ClaimRewards:
case Pox5EventName.ClaimStakerRewardsForSigner:
// TODO: Implement

Check warning on line 585 in src/datastore/pg-write-store.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO: Implement'
break;
case Pox5EventName.Stake:
case Pox5EventName.StakeUpdate:
Expand Down Expand Up @@ -649,7 +650,12 @@
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)}
Expand Down
17 changes: 8 additions & 9 deletions src/datastore/v3/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
16 changes: 16 additions & 0 deletions src/datastore/v3/helpers.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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[];
}
Loading
Loading