diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21b7938e8f..d744682a51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,7 @@ jobs: krypton:pox-4-stack-extend-increase, krypton:rpc, snp, + pox5:pox-transition, ] runs-on: ubuntu-latest steps: diff --git a/.vscode/launch.json b/.vscode/launch.json index 735ac1c3ea..71368d946f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -137,5 +137,24 @@ "NODE_ENV": "test" } }, + { + "type": "node", + "request": "launch", + "name": "test: pox5", + "runtimeExecutable": "node", + "args": [ + "--import", + "tsx", + "--test", + "--test-global-setup=./tests/pox5/setup.ts", + "--test-concurrency=1", + "./tests/pox5/pox-transition.test.ts" + ], + "outputCapture": "std", + "console": "integratedTerminal", + "env": { + "NODE_ENV": "test" + } + }, ], } diff --git a/package.json b/package.json index f33a51141c..31bb0a8f1a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:api:v2-proxy": "NODE_ENV=test node --import tsx --test --test-global-setup=./tests/api/setup.ts --test-concurrency=1 ./tests/api/v2-proxy/**/*.test.ts", "test:api:v3": "NODE_ENV=test node --import tsx --test --test-global-setup=./tests/api/setup.ts --test-concurrency=1 ./tests/api/v3/**/*.test.ts", "test:snp": "NODE_ENV=test node --import tsx --test --test-global-setup=./tests/snp/setup.ts --test-concurrency=1 ./tests/snp/**/*.test.ts", + "test:pox5:pox-transition": "NODE_ENV=test node --import tsx --test --test-global-setup=./tests/pox5/setup.ts --test-concurrency=1 ./tests/pox5/pox-transition.test.ts", "test:krypton:bns-e2e": "NODE_ENV=test node --import tsx --test --test-global-setup=./tests/krypton/setup.ts --test-concurrency=1 ./tests/krypton/bns-e2e/**/*.test.ts", "test:krypton:faucet-btc": "NODE_ENV=test node --import tsx --test --test-global-setup=./tests/krypton/setup.ts --test-concurrency=1 ./tests/krypton/faucet-btc/**/*.test.ts", "test:krypton:faucet-stx": "NODE_ENV=test node --import tsx --test --test-global-setup=./tests/krypton/setup.ts --test-concurrency=1 ./tests/krypton/faucet-stx/**/*.test.ts", diff --git a/src/api/serializers/v3/mempool-transactions.ts b/src/api/serializers/v3/mempool-transactions.ts index f4410dd371..b69d7c4377 100644 --- a/src/api/serializers/v3/mempool-transactions.ts +++ b/src/api/serializers/v3/mempool-transactions.ts @@ -93,7 +93,8 @@ export function serializeDbMempoolTransactionSummary( }; return tokenTransfer; } - case DbTxTypeId.SmartContract: { + case DbTxTypeId.SmartContract: + case DbTxTypeId.VersionedSmartContract: { const smartContract: SmartContractMempoolTransactionSummary = { ...result, type: 'smart_contract', @@ -122,7 +123,9 @@ export function serializeDbMempoolTransactionSummary( }; return poisonMicroblock; } - case DbTxTypeId.Coinbase: { + case DbTxTypeId.Coinbase: + case DbTxTypeId.NakamotoCoinbase: + case DbTxTypeId.CoinbaseToAltRecipient: { const coinbase: CoinbaseMempoolTransactionSummary = { ...result, type: 'coinbase', @@ -181,7 +184,8 @@ export function serializeDbMempoolTransaction( }; return tokenTransfer; } - case DbTxTypeId.SmartContract: { + case DbTxTypeId.SmartContract: + case DbTxTypeId.VersionedSmartContract: { const smartContract: SmartContractMempoolTransaction = { ...result, type: 'smart_contract', @@ -220,7 +224,9 @@ export function serializeDbMempoolTransaction( }; return poisonMicroblock; } - case DbTxTypeId.Coinbase: { + case DbTxTypeId.Coinbase: + case DbTxTypeId.NakamotoCoinbase: + case DbTxTypeId.CoinbaseToAltRecipient: { const coinbase: CoinbaseMempoolTransaction = { ...result, type: 'coinbase', diff --git a/src/api/serializers/v3/transactions.ts b/src/api/serializers/v3/transactions.ts index 0f473a1fe4..fe1137d1cd 100644 --- a/src/api/serializers/v3/transactions.ts +++ b/src/api/serializers/v3/transactions.ts @@ -134,7 +134,8 @@ export function serializeDbTransactionSummary(summary: DbTransactionSummary): Tr }; return tokenTransfer; } - case DbTxTypeId.SmartContract: { + case DbTxTypeId.SmartContract: + case DbTxTypeId.VersionedSmartContract: { const smartContract: SmartContractTransactionSummary = { ...result, type: 'smart_contract', @@ -163,7 +164,9 @@ export function serializeDbTransactionSummary(summary: DbTransactionSummary): Tr }; return poisonMicroblock; } - case DbTxTypeId.Coinbase: { + case DbTxTypeId.Coinbase: + case DbTxTypeId.NakamotoCoinbase: + case DbTxTypeId.CoinbaseToAltRecipient: { const coinbase: CoinbaseTransactionSummary = { ...result, type: 'coinbase', @@ -274,7 +277,8 @@ export function serializeDbTransaction( }; return tokenTransfer; } - case DbTxTypeId.SmartContract: { + case DbTxTypeId.SmartContract: + case DbTxTypeId.VersionedSmartContract: { const smartContract: SmartContractTransaction = { ...result, type: 'smart_contract', @@ -313,7 +317,9 @@ export function serializeDbTransaction( }; return poisonMicroblock; } - case DbTxTypeId.Coinbase: { + case DbTxTypeId.Coinbase: + case DbTxTypeId.NakamotoCoinbase: + case DbTxTypeId.CoinbaseToAltRecipient: { const coinbase: CoinbaseTransaction = { ...result, type: 'coinbase', diff --git a/tests/pox5/pox-transition.test.ts b/tests/pox5/pox-transition.test.ts new file mode 100644 index 0000000000..0a41b9c9f0 --- /dev/null +++ b/tests/pox5/pox-transition.test.ts @@ -0,0 +1,214 @@ +import { after, before, describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { + fetchGet, + getPox5Context, + standByForContract, + standByForPoxContractId, + standByUntilBurnBlock, + stopPox5Context, + waitFor, + type Pox5Context, +} from './pox5-env.ts'; +import { PoxContractIdentifier } from '../../src/event-stream/pox-constants.ts'; + +/** + * pox transition e2e — Stacks 2.x → 3.x (Nakamoto) → 4.x (pox-5). + * + * Walks the chain through the full epoch progression and verifies the + * event → DB → API pipeline keeps pace at every step: + * - 2.x: the `stacker` sidecar stacks via pox-4 to seat the signer set, and + * the `tx-broadcaster` issues stx-transfers — both ingest correctly. + * - 3.0 (Nakamoto): the chain enters Nakamoto and blocks keep being ingested. + * - 4.0 (pox-5): the active pox contract transitions to pox-5 and ingestion + * continues across the boundary. + * + * The pox-5-specific event/bond ingestion that the btc-staker triggers + * (registrations, stake → bond) is covered in a separate test. + */ + +// Epoch activation burn heights mirror the regtest-env compose `x-common-vars` +// defaults; override via env if you run a customized compose. +const EPOCH_30_BURN_HEIGHT = Number(process.env.STACKS_30_HEIGHT ?? 131); +const EPOCH_40_BURN_HEIGHT = Number(process.env.STACKS_40_HEIGHT ?? 141); + +// Generous ceiling: the chain must advance all the way to epoch 4.0 +// (~burn height 141), seating the signer set first. +const READY_TIMEOUT = 25 * 60_000; + +interface TransactionSummaryListResponse { + total: number; + results: { type: string; status: string; tx_id: string; sender: { address: string } }[]; +} + +describe('pox transition e2e — Stacks 2.x → 3.x → 4.x', () => { + let ctx: Pox5Context; + + before(async () => { + ctx = await getPox5Context(); + }, { timeout: READY_TIMEOUT }); + + after(async () => { + if (ctx) await stopPox5Context(ctx); + }); + + // ---- Stacks 2.x → 3.0 (Nakamoto) ---- + + test( + 'chain reaches Nakamoto (epoch 3.0) and the API ingests blocks', + { timeout: READY_TIMEOUT }, + async () => { + // Wait until the API has ingested a block at/after the epoch 3.0 burn + // height — proves block ingestion is keeping pace with the node. + const nakamotoBlock = await standByUntilBurnBlock(EPOCH_30_BURN_HEIGHT + 1, ctx); + assert.ok( + nakamotoBlock.burn_block_height >= EPOCH_30_BURN_HEIGHT, + `ingested block burn height ${nakamotoBlock.burn_block_height} >= ${EPOCH_30_BURN_HEIGHT}` + ); + + // Read the tip from the API dataset (chain_tip table), not the node RPC: + // the node can briefly report stacks_tip_height=0 while emitting a block-0 + // re-sync control message, which makes node-side getInfo() checks flaky. + const chainTip = await ctx.db.getChainTip(ctx.db.sql); + assert.ok( + chainTip.block_height > 0, + `API chain_tip stacks height ${chainTip.block_height} > 0` + ); + assert.ok( + chainTip.burn_block_height >= EPOCH_30_BURN_HEIGHT, + `API chain_tip burn height ${chainTip.burn_block_height} reached Nakamoto` + ); + } + ); + + test( + 'stacker pox-4 stack-stx txs are ingested as synthetic pox4 events', + { timeout: READY_TIMEOUT }, + async () => { + // The `stacker` sidecar stacks via pox-4 (the active contract across epoch + // 2.5–3.x) to seat the signer set that enables Nakamoto; those calls emit + // synthetic pox events into the pox4_events table. Their presence is the + // durable proof that the 2.x pox-4 phase happened, independent of the + // chain's current epoch. + const count = await waitFor('pox4_events to be recorded', async () => { + const rows = await ctx.db.sql<{ count: number }[]>` + SELECT COUNT(*)::int AS count FROM pox4_events WHERE canonical = TRUE + `; + return rows[0].count > 0 ? rows[0].count : undefined; + }); + assert.ok(count > 0, `found ${count} canonical pox4_events`); + } + ); + + test( + 'the pox-4 boot contract is recorded in smart_contracts', + { timeout: READY_TIMEOUT }, + async () => { + // pox-4 is not a genesis boot contract — it's deployed by the boot address + // at the epoch 2.5 activation, so it's present well before Nakamoto. + const pox4Id = PoxContractIdentifier.pox4.testnet; + await standByForContract(ctx, pox4Id); + const contract = await ctx.api.datastore.getSmartContract(pox4Id); + assert.ok(contract.found, `${pox4Id} present in smart_contracts`); + } + ); + + test( + 'tx-broadcaster STX token-transfer txs are ingested', + { timeout: READY_TIMEOUT }, + async () => { + // The `tx-broadcaster` sidecar issues periodic stx-transfers post-Nakamoto + // to drive block production; confirm they ingest and succeed. The v3 + // transactions list is cursor-paginated with no type filter, so filter the + // recent-tx page client-side. + const transfer = await waitFor('a successful token_transfer tx', async () => { + const res = await fetchGet( + '/extended/v3/transactions?limit=50', + ctx + ); + return res.results.find(t => t.type === 'token_transfer' && t.status === 'success'); + }); + assert.equal(transfer.type, 'token_transfer'); + assert.equal(transfer.status, 'success'); + } + ); + + // ---- 3.x → 4.0 (pox-5) ---- + + test( + 'the active pox contract transitions to pox-5 at epoch 4.0', + { timeout: READY_TIMEOUT }, + async () => { + const poxInfo = await standByForPoxContractId(ctx, 'pox-5', READY_TIMEOUT); + + // Active pox contract is now pox-5. + assert.match(poxInfo.contract_id, /\.pox-5$/); + + const burnHeight = poxInfo.current_burnchain_block_height ?? 0; + assert.ok( + burnHeight >= EPOCH_40_BURN_HEIGHT, + `burn height ${burnHeight} reached epoch 4.0 (${EPOCH_40_BURN_HEIGHT})` + ); + + // The node should advertise pox-5 as an activated contract version. + // const pox5Version = poxInfo.contract_versions?.find(v => v.contract_id.endsWith('.pox-5')); + // assert.ok(pox5Version, 'pox-5 present in contract_versions'); + // assert.ok( + // pox5Version.activation_burnchain_block_height <= burnHeight, + // `pox-5 activation height ${pox5Version.activation_burnchain_block_height} has been reached` + // ); + } + ); + + test( + 'the pox-5 boot contract is recorded in smart_contracts', + { timeout: READY_TIMEOUT }, + async () => { + // Like pox-4, pox-5 is deployed by the boot address at its epoch (4.0) + // activation, so it lands in smart_contracts once the transition occurs. + const pox5Id = PoxContractIdentifier.pox5.testnet; + await standByForContract(ctx, pox5Id); + const contract = await ctx.api.datastore.getSmartContract(pox5Id); + assert.ok(contract.found, `${pox5Id} present in smart_contracts`); + } + ); + + test( + 'pox5_events are recorded after epoch 4.0', + { timeout: READY_TIMEOUT }, + async () => { + // Once in epoch 4.0, the btc-staker registers signers and calls + // pox5.stake, which emits synthetic pox-5 events the API ingests into the + // pox5_events table. (May lag the transition by a few blocks while the + // btc-staker deploys its signer-manager and confirms registration.) + const count = await waitFor('pox5_events to be recorded', async () => { + const rows = await ctx.db.sql<{ count: number }[]>` + SELECT COUNT(*)::int AS count FROM pox5_events WHERE canonical = TRUE + `; + return rows[0].count > 0 ? rows[0].count : undefined; + }); + assert.ok(count > 0, `found ${count} canonical pox5_events`); + } + ); + + test( + 'the API keeps ingesting blocks across the transition into epoch 4.0', + { timeout: READY_TIMEOUT }, + async () => { + // The epoch 4.0 burn block (and beyond) must be ingested — i.e. the event + // pipeline didn't stall at the transition. + const epoch4Block = await standByUntilBurnBlock(EPOCH_40_BURN_HEIGHT, ctx); + assert.ok( + epoch4Block.burn_block_height >= EPOCH_40_BURN_HEIGHT, + `ingested block burn height ${epoch4Block.burn_block_height} >= ${EPOCH_40_BURN_HEIGHT}` + ); + + // Confirm the tip via the API dataset (chain_tip), not the node RPC. + const chainTip = await ctx.db.getChainTip(ctx.db.sql); + assert.ok( + chainTip.burn_block_height >= EPOCH_40_BURN_HEIGHT, + `API chain_tip burn height ${chainTip.burn_block_height} is in epoch 4.0` + ); + } + ); +}); diff --git a/tests/pox5/pox5-env.ts b/tests/pox5/pox5-env.ts new file mode 100644 index 0000000000..19faf9fd4d --- /dev/null +++ b/tests/pox5/pox5-env.ts @@ -0,0 +1,415 @@ +import { + bufferCV, + ClarityValue, + getAddressFromPrivateKey, + tupleCV, + TupleCV, +} from '@stacks/transactions'; +import { ENV } from '../../src/env.ts'; +import { EventStreamServer, startEventServer } from '../../src/event-stream/event-server.ts'; +import { migrate } from '../test-helpers.ts'; +import { PgWriteStore } from '../../src/datastore/pg-write-store.ts'; +import { ApiServer, startApiServer } from '../../src/api/init.ts'; +import { CoreRpcPoxInfo, StacksCoreRpcClient } from '../../src/core-rpc/client.ts'; +import { coerceToBuffer, timeout } from '@stacks/api-toolkit'; +import { ChainId, createNetwork, STACKS_TESTNET } from '@stacks/network'; +import type { StacksNetwork } from '@stacks/network'; +import { RPCClient } from 'rpc-bitcoin'; +import { ClarityTypeID, decodeClarityValue } from '@stacks/codec'; +import type { ClarityValue as DecodedClarityValue } from '@stacks/codec'; +import { decodeBtcAddress } from '@stacks/stacking'; +import { FAUCET_TESTNET_KEYS } from '../../src/api/routes/v1/faucets.ts'; +import { AddressStxBalance } from '../../src/api/schemas/v1/entities/addresses.ts'; +import { DbBlock, DbTx, DbTxStatus } from '../../src/datastore/common.ts'; +// Reuse the krypton bitcoin key/address helpers instead of duplicating them. +import { BitcoinAddressFormat, ECPair, getBitcoinAddressFromKey } from '../krypton/ec-helpers.ts'; +import { hexToBytes } from '@stacks/common'; +import supertest from 'supertest'; +import assert from 'node:assert/strict'; + +/** + * In-process test harness for the pox-5 / bitcoin-staking suite. + * + * `tests/pox5/setup.ts` (the global setup) owns the dockerized chain + * (bitcoind + stacks-node + signers + staking sidecars + postgres). This module + * starts the Stacks API + event server *in-process on the host* — the + * dockerized node posts events to `host.docker.internal:3700` and the staking + * sidecars query the API at `host.docker.internal:3999`, so the API must live on + * the host. This mirrors `krypton-env.ts`. + * + * Connection details come from `.env` (already pointed at the regtest stack: + * PG 5490, node RPC 20443, bitcoind 18443, event port 3700, chain 0x80000000). + */ +export interface Pox5Context { + db: PgWriteStore; + eventServer: EventStreamServer; + api: ApiServer; + client: StacksCoreRpcClient; + stacksNetwork: StacksNetwork; + bitcoinRpcClient: RPCClient; +} + +export type Account = { + secretKey: string; + pubKey: string; + stxAddr: string; + btcAddr: string; + btcTestnetAddr: string; + poxAddr: { version: number; data: string }; + poxAddrClar: TupleCV; + wif: string; +}; + +export function accountFromKey( + privateKey: string, + addressFormat: BitcoinAddressFormat = 'p2pkh' +): Account { + const privKeyBuff = coerceToBuffer(privateKey); + if (privKeyBuff.byteLength !== 33) { + throw new Error('Only compressed private keys supported'); + } + const ecPair = ECPair.fromPrivateKey(privKeyBuff.slice(0, 32), { compressed: true }); + const secretKey = ecPair.privateKey!.toString('hex') + '01'; + if (secretKey.slice(0, 64) !== privateKey.slice(0, 64)) { + throw new Error(`key mismatch`); + } + const pubKey = ecPair.publicKey.toString('hex'); + const stxAddr = getAddressFromPrivateKey(secretKey, 'testnet'); + const btcAccount = getBitcoinAddressFromKey({ + privateKey: ecPair.privateKey!, + network: 'regtest', + addressFormat, + verbose: true, + }); + const btcAddr = btcAccount.address; + const poxAddr = decodeBtcAddress(btcAddr); + const poxAddrClar = tupleCV({ + hashbytes: bufferCV(hexToBytes(poxAddr.data)), + version: bufferCV(Buffer.from([poxAddr.version])), + }); + const wif = btcAccount.wif; + const btcTestnetAddr = getBitcoinAddressFromKey({ + privateKey: ecPair.privateKey!, + network: 'testnet', + addressFormat, + }); + return { secretKey, pubKey, stxAddr, poxAddr, poxAddrClar, btcAddr, btcTestnetAddr, wif }; +} + +// -- Stand-by helpers (poll the API datastore / node RPC for chain progress) -- + +/** + * Generic polling helper: repeatedly invoke `fn` until it returns a truthy + * value (which is then returned), or throw once `timeoutMs` elapses. Logs a + * heartbeat with `desc` every 15s so long waits don't look frozen. + * + * Useful whenever a sidecar acts on its own schedule and there's no specific + * txid/height to wait on — e.g. "wait until some bond exists", "wait until a + * token-transfer shows up". + */ +export async function waitFor( + desc: string, + fn: () => Promise | T | undefined | null | false, + timeoutMs = 15 * 60_000 +): Promise { + const deadline = Date.now() + timeoutMs; + let lastLog = 0; + while (true) { + const result = await fn(); + if (result) { + return result as T; + } + if (Date.now() > deadline) { + throw new Error(`Timed out waiting for: ${desc}`); + } + if (Date.now() - lastLog >= 15_000) { + console.log(`Waiting for: ${desc}…`); + lastLog = Date.now(); + } + await timeout(1_000); + } +} + +export async function standByUntilBurnBlock( + burnBlockHeight: number, + ctx: Pox5Context +): Promise { + let blockFound = false; + const dbBlock = await new Promise(async resolve => { + while (!blockFound) { + const dbBlock = await ctx.api.datastore.getBlockByBurnBlockHeight(burnBlockHeight); + if (dbBlock.found) { + blockFound = true; + resolve(dbBlock.result); + } else { + await timeout(50); + } + } + }); + return dbBlock; +} + +export async function standByUntilBlock(blockHeight: number, ctx: Pox5Context): Promise { + let blockFound = false; + const dbBlock = await new Promise(async resolve => { + while (!blockFound) { + const dbBlock = await ctx.api.datastore.getBlock({ height: blockHeight }); + if (dbBlock.found) { + blockFound = true; + resolve(dbBlock.result); + } else { + await timeout(50); + } + } + }); + return dbBlock; +} + +export async function standByForTx(expectedTxId: string, ctx: Pox5Context): Promise { + console.log(`Waiting for TX: ${expectedTxId}...`); + const tx = await new Promise(async resolve => { + let found = false; + do { + const dbTxQuery = await ctx.api.datastore.getTx({ + txId: expectedTxId, + includeUnanchored: false, + }); + if (dbTxQuery.found) { + found = true; + console.log(`Found TX: ${expectedTxId}`); + resolve(dbTxQuery.result); + } else { + await timeout(100); + } + } while (!found); + }); + return tx; +} + +export async function standByForTxSuccess(expectedTxId: string, ctx: Pox5Context): Promise { + const tx = await standByForTx(expectedTxId, ctx); + if (tx.status !== DbTxStatus.Success) { + const resultRepr = decodeClarityValue(tx.raw_result).repr; + throw new Error(`Tx failed with status ${tx.status}, result: ${resultRepr}`); + } + return tx; +} + +/** Stand by until the prepare phase of the next pox cycle (still in current cycle). */ +export async function standByForNextPoxCycle(ctx: Pox5Context): Promise { + const firstPoxInfo = await ctx.client.getPox(); + await standByUntilBurnBlock(firstPoxInfo.next_cycle.prepare_phase_start_block_height, ctx); + return await ctx.client.getPox(); +} + +/** Stand by until the burn height reaches the start of the next cycle. */ +export async function standByForPoxCycle(ctx: Pox5Context): Promise { + const firstPoxInfo = await ctx.client.getPox(); + let lastPoxInfo: CoreRpcPoxInfo = JSON.parse(JSON.stringify(firstPoxInfo)); + do { + await standByUntilBurnBlock(lastPoxInfo.current_burnchain_block_height! + 1, ctx); + lastPoxInfo = await ctx.client.getPox(); + } while ( + (lastPoxInfo.current_burnchain_block_height as number) <= + firstPoxInfo.next_cycle.reward_phase_start_block_height + ); + return lastPoxInfo; +} + +export async function standByForAccountUnlock(address: string, ctx: Pox5Context): Promise { + while (true) { + const accountInfo = await ctx.client.getAccount(address); + if (BigInt(accountInfo.locked) === 0n) { + break; + } + const info = await ctx.client.getInfo(); + await standByUntilBlock(info.stacks_tip_height + 1, ctx); + } +} + +/** + * Poll the node's `/v2/pox` until the active pox contract id ends with the given + * contract name (e.g. `'pox-4'` pre-transition, `'pox-5'` after epoch 4.0). + * Robust alternative to hardcoding epoch burn heights. + */ +export async function standByForPoxContractId( + ctx: Pox5Context, + contractName: string, + timeoutMs = 15 * 60_000 +): Promise { + const deadline = Date.now() + timeoutMs; + let lastLog = 0; + while (true) { + const pox = await ctx.client.getPox(); + if (pox.contract_id.endsWith(`.${contractName}`)) { + return pox; + } + if (Date.now() > deadline) { + throw new Error( + `Timed out waiting for pox contract '${contractName}' (current: ${pox.contract_id})` + ); + } + if (Date.now() - lastLog >= 15_000) { + console.log( + `Waiting for pox contract '${contractName}' (current ${pox.contract_id} @ burn height ${pox.current_burnchain_block_height})…` + ); + lastLog = Date.now(); + } + await timeout(2_000); + } +} + +/** + * Poll the datastore until a contract-deploy tx with the given id has been + * ingested. The staking sidecars deploy contracts autonomously (so we can't + * predict their txids), but the contract ids are deterministic + * (`.`), so we wait by id rather than by txid. + */ +export async function standByForContract( + ctx: Pox5Context, + contractId: string, + timeoutMs = 15 * 60_000 +): Promise { + const deadline = Date.now() + timeoutMs; + let lastLog = 0; + while (true) { + const res = await ctx.api.datastore.getSmartContract(contractId); + if (res.found) { + return; + } + if (Date.now() > deadline) { + throw new Error(`Timed out waiting for contract ${contractId} to be ingested`); + } + if (Date.now() - lastLog >= 15_000) { + console.log(`Waiting for contract ${contractId} to be ingested…`); + lastLog = Date.now(); + } + await timeout(1_000); + } +} + +// -- HTTP / read-only call helpers -- + +export async function fetchGet(endpoint: string, ctx: Pox5Context): Promise { + const result = await supertest(ctx.api.server).get(endpoint); + // Follow redirects + if (result.status >= 300 && result.status < 400) { + return await fetchGet(result.header.location, ctx); + } + assert.equal(result.status, 200); + assert.equal(result.type, 'application/json'); + return result.body as TRes; +} + +export async function readOnlyFnCall( + contract: string | [string, string], + fnName: string, + ctx: Pox5Context, + args?: ClarityValue[], + sender?: string, + unwrap = true +): Promise { + const [contractAddr, contractName] = + typeof contract === 'string' ? contract.split('.') : contract; + const callResp = await ctx.client.sendReadOnlyContractCall( + contractAddr, + contractName, + fnName, + sender ?? FAUCET_TESTNET_KEYS[0].stacksAddress, + args ?? [] + ); + if (!callResp.okay) { + throw new Error(`Failed to call ${contract}::${fnName}`); + } + const decodedVal = decodeClarityValue(callResp.result); + if (unwrap) { + if (decodedVal.type_id === ClarityTypeID.OptionalSome) return decodedVal.value as T; + if (decodedVal.type_id === ClarityTypeID.ResponseOk) return decodedVal.value as T; + if (decodedVal.type_id === ClarityTypeID.OptionalNone) { + throw new Error(`OptionNone result for call to ${contract}::${fnName}`); + } + if (decodedVal.type_id === ClarityTypeID.ResponseError) { + throw new Error(`ResultError result for call to ${contract}::${fnName}: ${decodedVal.repr}`); + } + } + return decodedVal; +} + +// -- Context lifecycle -- + +/** + * Wait for the dockerized node RPC to be reachable. + * + * This is the real readiness gate for the suite, and it MUST run after the + * event server is listening (see `getPox5Context`): the node won't progress + * until it can deliver events to host:3700, so it only becomes RPC-responsive + * once that server is up to drain its (possibly buffered) events. Deadline-based + * with progress logging since a cold node boot can take a while. + */ +async function standByForNodeReady( + client: StacksCoreRpcClient, + timeoutMs = 5 * 60_000 +): Promise { + const deadline = Date.now() + timeoutMs; + let lastLog = 0; + while (true) { + try { + await client.getInfo(); + return; + } catch (error) { + if (Date.now() > deadline) { + throw new Error(`Stacks node RPC not reachable within ${timeoutMs}ms: ${error}`); + } + if (Date.now() - lastLog >= 15_000) { + console.log('Waiting for stacks-node RPC (event server is up, draining node events)…'); + lastLog = Date.now(); + } + await timeout(1000); + } + } +} + +export async function getPox5Context(): Promise { + process.env.PG_DATABASE = 'postgres'; + ENV.PG_DATABASE = 'postgres'; + ENV.STACKS_CHAIN_ID = '0x80000000'; + + await migrate('up'); + const db = await PgWriteStore.connect({ usageName: 'tests' }); + const eventServer = await startEventServer({ + datastore: db, + chainId: ChainId.Testnet, + serverHost: '0.0.0.0', + serverPort: 3700, + }); + const api = await startApiServer({ datastore: db, writeDatastore: db, chainId: ChainId.Testnet }); + const client = new StacksCoreRpcClient({ host: '127.0.0.1', port: 20443 }); + const stacksNetwork = createNetwork({ + network: STACKS_TESTNET, + client: { baseUrl: `http://${client.endpoint}` }, + }); + const bitcoinRpcClient = new RPCClient({ + url: ENV.BTC_RPC_HOST, + port: ENV.BTC_RPC_PORT, + user: ENV.BTC_RPC_USER, + pass: ENV.BTC_RPC_PW ?? '', + timeout: 120000, + wallet: 'main', + }); + + const ctx: Pox5Context = { db, eventServer, client, stacksNetwork, bitcoinRpcClient, api }; + + try { + await standByForNodeReady(client); + return ctx; + } catch (error) { + await stopPox5Context(ctx); + throw error; + } +} + +export async function stopPox5Context(ctx: Pox5Context): Promise { + await ctx.api.forceKill(); + await ctx.eventServer.closeAsync(); + await ctx.db?.close({ timeout: 0 }); +} diff --git a/tests/pox5/regtest-env/Dockerfile.btc b/tests/pox5/regtest-env/Dockerfile.btc new file mode 100644 index 0000000000..d581ebb0c9 --- /dev/null +++ b/tests/pox5/regtest-env/Dockerfile.btc @@ -0,0 +1,7 @@ +FROM debian:bookworm + +COPY --from=dobtc/bitcoin:25.1 /opt/bitcoin-*/bin /usr/local/bin + +RUN apt-get update && apt-get install -y curl jq zstd +RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +CMD ["/bin/bash"] diff --git a/tests/pox5/regtest-env/Dockerfile.stacker b/tests/pox5/regtest-env/Dockerfile.stacker new file mode 100644 index 0000000000..21e3d3902f --- /dev/null +++ b/tests/pox5/regtest-env/Dockerfile.stacker @@ -0,0 +1,14 @@ +FROM node:20-bookworm AS builder + +RUN apt-get update && apt-get install -y curl gettext-base jq + +WORKDIR /root +COPY ./stacking/package.json /root/ +RUN npm i + +COPY ./stacking/contracts/*.clar /root/contracts/ +COPY ./stacking/*.ts /root/ + +ENV NODE_OPTIONS=--enable-source-maps + +CMD ["./node_modules/.bin/tsx", "/root/stacking.ts"] diff --git a/tests/pox5/regtest-env/Dockerfile.stacks-node b/tests/pox5/regtest-env/Dockerfile.stacks-node new file mode 100644 index 0000000000..f7b7bb8877 --- /dev/null +++ b/tests/pox5/regtest-env/Dockerfile.stacks-node @@ -0,0 +1,33 @@ +FROM rust:bookworm AS builder + +RUN apt-get update && apt-get install -y libclang-dev +RUN rustup toolchain install stable +RUN rustup component add rustfmt --toolchain stable + +ARG GIT_COMMIT +RUN test -n "$GIT_COMMIT" || (echo "GIT_COMMIT not set" && false) +RUN echo "Building stacks-node from commit: https://github.com/stacks-network/stacks-blockchain/commit/$GIT_COMMIT" + +WORKDIR /stacks +RUN git init && \ + git remote add origin https://github.com/stacks-network/stacks-blockchain.git && \ + git -c protocol.version=2 fetch --depth=1 origin "$GIT_COMMIT" && \ + git reset --hard FETCH_HEAD + +RUN --mount=type=cache,target=/stacks/target \ + --mount=type=cache,target=/usr/local/cargo/registry \ + cargo build --package stacks-node --package stacks-signer --bin stacks-node --bin stacks-signer --features slog_json && \ + cp target/debug/stacks-node target/debug/stacks-signer /usr/local/bin/ + +FROM debian:bookworm + +COPY --from=builder /usr/local/bin/stacks-node /usr/local/bin/ +COPY --from=builder /usr/local/bin/stacks-signer /usr/local/bin/ + +COPY --from=dobtc/bitcoin:25.1 /opt/bitcoin-*/bin /usr/local/bin + +RUN apt-get update && apt-get install -y curl gettext-base jq +RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +WORKDIR /root +CMD ["stacks-node"] diff --git a/tests/pox5/regtest-env/bitcoin.conf b/tests/pox5/regtest-env/bitcoin.conf new file mode 100644 index 0000000000..544abbf296 --- /dev/null +++ b/tests/pox5/regtest-env/bitcoin.conf @@ -0,0 +1,36 @@ +regtest=1 #chain=regtest + +[regtest] +printtoconsole=1 +disablewallet=0 +txindex=1 +coinstatsindex=1 + +# Specify a non-default location to store blockchain data. +blocksdir=/chainstate/bitcoin-data +# Specify a non-default location to store blockchain and other data. +datadir=/chainstate/bitcoin-data + +# [network] +# bind=0.0.0.0:18444 +discover=0 +dns=0 +dnsseed=0 +listenonion=0 + +# [rpc] +rpcserialversion=0 +# Accept command line and JSON-RPC commands. +server=1 +# Accept public REST requests. +rest=1 +rpcbind=0.0.0.0:18443 +rpcallowip=0.0.0.0/0 +rpcallowip=::/0 +rpcuser=btc +rpcpassword=btc + +# [wallet] +addresstype=legacy +changetype=legacy +fallbackfee=0.00001 diff --git a/tests/pox5/regtest-env/docker-compose.yml b/tests/pox5/regtest-env/docker-compose.yml new file mode 100644 index 0000000000..d0a92743f0 --- /dev/null +++ b/tests/pox5/regtest-env/docker-compose.yml @@ -0,0 +1,503 @@ +x-common-vars: + - &STACKS_BLOCKCHAIN_COMMIT 6970f0cc02fc1e9b9ee1340918d477b13cfa0edc # feat/epoch-4-rc + - &STACKS_API_COMMIT fc47a7cd054018d8ea098fbb3c8db207dbf7c8c9 # 9.0.0-pox5.1 + - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT + - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za + - &BITCOIN_PEER_PORT 18444 + - &BITCOIN_RPC_PORT 18443 + - &BITCOIN_RPC_USER btc + - &BITCOIN_RPC_PASS btc + - &MINE_INTERVAL ${MINE_INTERVAL:-1s} + - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-2s} # bitcoin block times in epoch 2.5 + - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-1s} # bitcoin block times in epoch 3 + - &MINE_INTERVAL_EPOCH40 ${MINE_INTERVAL_EPOCH40:-2s} # bitcoin block times in epoch 4 + - &NAKAMOTO_BLOCK_INTERVAL 2 # seconds to wait between issuing stx-transfer transactions (which triggers Nakamoto block production) + - &STACKS_20_HEIGHT ${STACKS_20_HEIGHT:-0} + - &STACKS_2_05_HEIGHT ${STACKS_2_05_HEIGHT:-102} + - &STACKS_21_HEIGHT ${STACKS_21_HEIGHT:-103} + - &STACKS_POX2_HEIGHT ${STACKS_POX2_HEIGHT:-104} # 104 is is stacks_block=1, 106 is stacks_block=3 + - &STACKS_22_HEIGHT ${STACKS_22_HEIGHT:-105} + - &STACKS_23_HEIGHT ${STACKS_23_HEIGHT:-106} + - &STACKS_24_HEIGHT ${STACKS_24_HEIGHT:-107} + - &STACKS_25_HEIGHT ${STACKS_25_HEIGHT:-108} + - &STACKS_30_HEIGHT ${STACKS_30_HEIGHT:-131} + - &STACKS_31_HEIGHT ${STACKS_31_HEIGHT:-132} + - &STACKS_32_HEIGHT ${STACKS_32_HEIGHT:-133} + - &STACKS_33_HEIGHT ${STACKS_33_HEIGHT:-134} + - &STACKS_34_HEIGHT ${STACKS_34_HEIGHT:-135} + - &STACKS_40_HEIGHT ${STACKS_40_HEIGHT:-141} + - &STACKING_CYCLES ${STACKING_CYCLES:-1} # number of cycles to stack-stx or stack-extend for + - &STACKING_CYCLES_POX_5 ${STACKING_CYCLES_POX_5:-1} # number of cycles to stack-stx or stack-extend for + - &POX_PREPARE_LENGTH ${POX_PREPARE_LENGTH:-5} + - &POX_REWARD_LENGTH ${POX_REWARD_LENGTH:-20} + - &REWARD_RECIPIENT ${REWARD_RECIPIENT:-STQM73RQC4EX0A07KWG1J5ECZJYBZS4SJ4ERC6WN} # priv: 6ad9cadb42d4edbfbe0c5bfb3b8a4125ddced021c4174f829b714ccbf527f02001 + # Choose one to override the default + - &STACKS_CHAIN_ID ${STACKS_CHAIN_ID:-0x80000000} + # - &STACKS_CHAIN_ID ${STACKS_CHAIN_ID:-0x80000100} + - &CUSTOM_CHAIN_IDS ${CUSTOM_CHAIN_IDS:-testnet=0x55005500,mainnet=12345678,mainnet=0xdeadbeaf,testnet=0x80000100} + +services: + bitcoind: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.btc + ports: + - "18443:18443" + - "18444:18444" + volumes: + - ./bitcoin.conf:/root/.bitcoin/bitcoin.conf + - ./init-data:/init-data + - chainstate:/chainstate + environment: + DATA_DIR: /chainstate/bitcoin-data + entrypoint: + - /bin/bash + - -c + - | + set -e + mkdir -p $${DATA_DIR} + rm -rf $${DATA_DIR}/* + bitcoind + + bitcoind-miner: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.btc + depends_on: + - bitcoind + volumes: + - ./bitcoin.conf:/root/.bitcoin/bitcoin.conf + environment: + BITCOIN_ADDRESSES: *BITCOIN_ADDRESSES + MINE_INTERVAL: *MINE_INTERVAL + MINE_INTERVAL_EPOCH3: *MINE_INTERVAL_EPOCH3 + MINE_INTERVAL_EPOCH25: *MINE_INTERVAL_EPOCH25 + MINE_INTERVAL_EPOCH40: *MINE_INTERVAL_EPOCH40 + INIT_BLOCKS: 101 + STACKS_30_HEIGHT: *STACKS_30_HEIGHT + STACKS_25_HEIGHT: *STACKS_25_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT + entrypoint: + - /bin/bash + - -c + - | + set -e + trap "exit" INT TERM + trap "kill 0" EXIT + + OLD_IFS="$${IFS}" + IFS=',' + read -ra BITCOIN_ADDRESSES_ARRAY <<< "$${BITCOIN_ADDRESSES}" + IFS="$${OLD_IFS}" + + # Install dependencies + apt update && apt install -y jq bc + + # Wait for bitcoin-core container to start + sleep 5 + + echo "Checking if the main wallet exists" + if ! bitcoin-cli -rpcconnect=bitcoind listwallets | grep -q "main"; then + echo "Generating wallet and initial set of blocks" + bitcoin-cli -rpcconnect=bitcoind -rpcwait getmininginfo + bitcoin-cli -rpcconnect=bitcoind -named createwallet wallet_name="main" descriptors=false load_on_startup=true + fi + + # Generate a new or use an existing address for the Bitcoin miner + BITCOIN_MINER_ADDRESS=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="main" getnewaddress "btc_miner") + echo "Bitcoin miner address: $${BITCOIN_MINER_ADDRESS}" + + echo "Importing addresses and generating blocks" + # Import the addresses from BITCOIN_ADDRESSES_ARRAY + i=0 + for bitcoin_address in "$${BITCOIN_ADDRESSES_ARRAY[@]}"; do + # Create a unique wallet name for each address + wallet_name="stx_miner_$${i}" + + # Check if the wallet already exists + if ! bitcoin-cli -rpcconnect=bitcoind listwallets | grep -q "$${wallet_name}"; then + # Create a new wallet with the unique name + bitcoin-cli -rpcconnect=bitcoind -named createwallet wallet_name="$${wallet_name}" descriptors=false load_on_startup=true + echo "Wallet $${wallet_name} created." + else + echo "Wallet $${wallet_name} already exists." + fi + + echo "Importing $${bitcoin_address} into wallet $${wallet_name}" + + # Import the address into the newly created wallet + bitcoin-cli \ + -rpcconnect=bitcoind \ + -rpcwallet="$${wallet_name}" \ + -named importaddress \ + address="$${bitcoin_address}" \ + rescan=false \ + label="user_addresses" + + # Generate 1 block to fund the address + bitcoin-cli -rpcconnect=bitcoind -rpcwallet="$${wallet_name}" -named generatetoaddress nblocks=1 address="$${bitcoin_address}" + + # Increment the counter + i=$$((i + 1)) + done + + # Create the btc_staking wallet (used by the btc-staker container) + if ! bitcoin-cli -rpcconnect=bitcoind listwallets | grep -q "btc_staking"; then + bitcoin-cli -rpcconnect=bitcoind -named createwallet wallet_name="btc_staking" descriptors=false load_on_startup=true + echo "Created btc_staking wallet" + fi + STAKING_WALLET_ADDRESS=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="btc_staking" getnewaddress "staking_fund") + + # Generate the initial blocks to fund the Bitcoin miner address + bitcoin-cli -rpcconnect=bitcoind -rpcwallet="main" -named generatetoaddress nblocks=$${INIT_BLOCKS} address="$${BITCOIN_MINER_ADDRESS}" + + # Fund the staking wallet + bitcoin-cli -rpcconnect=bitcoind -rpcwallet="main" -named sendtoaddress address="$${STAKING_WALLET_ADDRESS}" amount=10.0 + echo "Funded btc_staking wallet with 10 BTC" + + DEFAULT_TIMEOUT=$$(($$(date +%s) + 30)) + while true; do + TX_FOUND=false + + i=0 + for bitcoin_address in "$${BITCOIN_ADDRESSES_ARRAY[@]}"; do + wallet_name="stx_miner_$${i}" + + # Get the most recent transaction for the wallet + TX=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="$${wallet_name}" -named listtransactions label='*' count=1 include_watchonly=true) + + # Check if there is a transaction + if [ "$${TX}" != "[]" ]; then + # Extract the confirmations + CONFS=$$(echo "$${TX}" | jq '.[0].confirmations') + + # Check if there is a transaction with 0 confirmations + if [ "$${CONFS}" = "0" ]; then + echo "Detected unconfirmed transaction in wallet $${wallet_name} (address $${bitcoin_address})" + TX_FOUND=true + fi + else + echo "No transactions found in wallet $${wallet_name}" + fi + + # Check this Stacks miner's Bitcoin balance and top it up if it is low + BALANCE=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="$${wallet_name}" getbalances | jq '.watchonly.trusted + .watchonly.untrusted_pending') + BALANCE=$${BALANCE:-0} + + if (( $$(echo "$${BALANCE} < 0.1" | bc -l) )); then + echo "Balance of $${bitcoin_address} is low ($${BALANCE} BTC), sending 1 BTC from miner $${BITCOIN_MINER_ADDRESS}." + + # Send 1 BTC from BITCOIN_MINER_ADDRESS wallet to bitcoin_address + TXID=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="main" -named sendtoaddress \ + address="$${bitcoin_address}" \ + amount=1.0 \ + subtractfeefromamount=true \ + fee_rate=1) + + echo "Transaction ID: $${TXID}" + fi + + i=$$((i + 1)) + done + + # Top up btc_staking wallet if low (include unconfirmed balance) + STAKING_BALANCE=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="btc_staking" getbalances | jq '.mine.trusted + .mine.untrusted_pending') + STAKING_BALANCE=$${STAKING_BALANCE:-0} + if (( $$(echo "$${STAKING_BALANCE} < 1.0" | bc -l) )); then + STAKING_ADDR=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="btc_staking" getnewaddress "staking_topup") + bitcoin-cli -rpcconnect=bitcoind -rpcwallet="main" -named sendtoaddress address="$${STAKING_ADDR}" amount=5.0 + echo "Topped up btc_staking wallet (was $${STAKING_BALANCE} BTC)" + fi + + # Check if any unconfirmed transactions were found or if the timeout has been reached + if [ "$${TX_FOUND}" = true ] || [ $$(date +%s) -gt $${DEFAULT_TIMEOUT} ]; then + if [ $$(date +%s) -gt $${DEFAULT_TIMEOUT} ]; then + echo "Timed out waiting for a mempool tx, mining a btc block..." + else + echo "Detected Stacks mining mempool tx, mining btc block..." + fi + bitcoin-cli -rpcconnect=bitcoind -named -rpcwallet="main" -named generatetoaddress nblocks=1 address="$${BITCOIN_MINER_ADDRESS}" + DEFAULT_TIMEOUT=$(($(date +%s) + 30)) + else + echo "No Stacks mining tx detected" + fi + + SLEEP_DURATION=$${MINE_INTERVAL} + BLOCK_HEIGHT=$$(bitcoin-cli -rpcconnect=bitcoind getblockcount) + if [ "$${BLOCK_HEIGHT}" -ge "$${STACKS_40_HEIGHT}" ]; then + echo "In Epoch4.0, sleeping for $${MINE_INTERVAL_EPOCH40} ..." + SLEEP_DURATION=$${MINE_INTERVAL_EPOCH40} + elif [ "$${BLOCK_HEIGHT}" -ge $$(( $${STACKS_30_HEIGHT} - 1 )) ]; then + echo "In Epoch3, sleeping for $${MINE_INTERVAL_EPOCH3} ..." + SLEEP_DURATION=$${MINE_INTERVAL_EPOCH3} + elif [ "$${BLOCK_HEIGHT}" -ge "$${STACKS_25_HEIGHT}" ]; then + echo "In Epoch2.5, sleeping for $${MINE_INTERVAL_EPOCH25} ..." + SLEEP_DURATION=$${MINE_INTERVAL_EPOCH25} + fi + sleep $${SLEEP_DURATION} & + wait || exit 0 + done + + stacks-node: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.stacks-node + args: + GIT_COMMIT: *STACKS_BLOCKCHAIN_COMMIT + depends_on: + - bitcoind + extra_hosts: + # Lets the node post events to the Stacks API running manually on the host + - "host.docker.internal:host-gateway" + ports: + - "20443:20443" + volumes: + - ./stacks-krypton-miner.toml/:/root/config.toml.in + - ./bitcoin.conf:/root/.bitcoin/bitcoin.conf + - ./init-data:/init-data + - chainstate:/chainstate + environment: + # STACKS_LOG_TRACE: 1 # uncomment for trace logging + # STACKS_LOG_DEBUG: 1 + # RUST_LOG: debug + DATA_DIR: /chainstate/stacks-blockchain-miner-data + BITCOIN_PEER_HOST: bitcoind + BITCOIN_PEER_PORT: *BITCOIN_PEER_PORT + BITCOIN_RPC_PORT: *BITCOIN_RPC_PORT + BITCOIN_RPC_USER: *BITCOIN_RPC_USER + BITCOIN_RPC_PASS: *BITCOIN_RPC_PASS + MINER_SEED: *MINER_SEED + STACKS_20_HEIGHT: *STACKS_20_HEIGHT + STACKS_2_05_HEIGHT: *STACKS_2_05_HEIGHT + STACKS_21_HEIGHT: *STACKS_21_HEIGHT + STACKS_POX2_HEIGHT: *STACKS_POX2_HEIGHT + STACKS_22_HEIGHT: *STACKS_22_HEIGHT + STACKS_23_HEIGHT: *STACKS_23_HEIGHT + STACKS_24_HEIGHT: *STACKS_24_HEIGHT + STACKS_25_HEIGHT: *STACKS_25_HEIGHT + STACKS_30_HEIGHT: *STACKS_30_HEIGHT + STACKS_31_HEIGHT: *STACKS_31_HEIGHT + STACKS_32_HEIGHT: *STACKS_32_HEIGHT + STACKS_33_HEIGHT: *STACKS_33_HEIGHT + STACKS_34_HEIGHT: *STACKS_34_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT + POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH + POX_REWARD_LENGTH: *POX_REWARD_LENGTH + REWARD_RECIPIENT: *REWARD_RECIPIENT + STACKS_CHAIN_ID: *STACKS_CHAIN_ID + STACKS_LOG_JSON: 1 + entrypoint: + - /bin/bash + - -c + - | + set -e + if [[ ! -z "$${REWARD_RECIPIENT}" ]]; then + export REWARD_RECIPIENT_CONF="block_reward_recipient = \"$${REWARD_RECIPIENT}\"" + fi + mkdir -p $${DATA_DIR} + rm -rf $${DATA_DIR}/* + envsubst < config.toml.in > config.toml + echo "Starting Stacks with config: $(cat config.toml)" + bitcoin-cli -rpcwait -rpcconnect=bitcoind getmininginfo + exec stacks-node start --config config.toml + + stacker: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.stacker + environment: + STACKS_CORE_RPC_HOST: stacks-node + STACKS_CORE_RPC_PORT: 20443 + STACKING_CYCLES: *STACKING_CYCLES + STACKING_CYCLES_POX_5: *STACKING_CYCLES_POX_5 + STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 + STACKS_25_HEIGHT: *STACKS_25_HEIGHT + STACKS_30_HEIGHT: *STACKS_30_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT + POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH + POX_REWARD_LENGTH: *POX_REWARD_LENGTH + STACKS_CHAIN_ID: *STACKS_CHAIN_ID + STACKING_INTERVAL: 2 # interval (seconds) for checking if stacking transactions are needed + POST_TX_WAIT: 10 # seconds to wait after a stacking transaction broadcast before continuing the loop + SERVICE_NAME: stacker + NODE_OPTIONS: --enable-source-maps + depends_on: + - stacks-node + + btc-staker: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.stacker + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + # Points at the Stacks API running manually on the host (see disabled stacks-api service) + STACKS_CORE_RPC_HOST: host.docker.internal + STACKS_CORE_RPC_PORT: 3999 + STACKING_CYCLES: *STACKING_CYCLES + STACKING_CYCLES_POX_5: *STACKING_CYCLES_POX_5 + STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 + STACKS_25_HEIGHT: *STACKS_25_HEIGHT + STACKS_30_HEIGHT: *STACKS_30_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT + POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH + POX_REWARD_LENGTH: *POX_REWARD_LENGTH + STACKS_CHAIN_ID: *STACKS_CHAIN_ID + STACKING_INTERVAL: 2 + POST_TX_WAIT: 10 + SERVICE_NAME: btc-staker + NODE_OPTIONS: --enable-source-maps + BITCOIN_RPC_HOST: bitcoind + BITCOIN_RPC_PORT: *BITCOIN_RPC_PORT + BITCOIN_RPC_USER: *BITCOIN_RPC_USER + BITCOIN_RPC_PASS: *BITCOIN_RPC_PASS + BTC_LOCK_AMOUNT_SATS: 100000 + depends_on: + - stacks-node + - bitcoind + entrypoint: + - /bin/bash + - -c + - | + set -e + exec ./node_modules/.bin/tsx /root/btc-staker.ts + + tx-broadcaster: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.stacker + environment: + STACKS_CORE_RPC_HOST: stacks-node + STACKS_CORE_RPC_PORT: 20443 + NAKAMOTO_BLOCK_INTERVAL: *NAKAMOTO_BLOCK_INTERVAL + STACKS_30_HEIGHT: *STACKS_30_HEIGHT + ACCOUNT_KEYS: 0d2f965b472a82efd5a96e6513c8b9f7edc725d5c96c7d35d6c722cedeb80d1b01,975b251dd7809469ef0c26ec3917971b75c51cd73a022024df4bf3b232cc2dc001,c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01 + STACKS_25_HEIGHT: *STACKS_25_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT + POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH + POX_REWARD_LENGTH: *POX_REWARD_LENGTH + STACKS_CHAIN_ID: *STACKS_CHAIN_ID + STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 + NODE_OPTIONS: --enable-source-maps + depends_on: + - stacks-node + entrypoint: + - /bin/bash + - -c + - | + set -e + exec ./node_modules/.bin/tsx /root/tx-broadcaster.ts + + postgres: + networks: + - stacks + image: "postgres:15" + pull_policy: if_not_present + ports: + - "5490:5490" + volumes: + - chainstate:/chainstate + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: stacks_blockchain_api + PGPORT: 5490 + PGDATA: /chainstate/pg-data + + stacks-signer-1: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.stacks-node + args: + GIT_COMMIT: *STACKS_BLOCKCHAIN_COMMIT + depends_on: + - stacks-node + volumes: + - ./signer-0.toml:/root/config.toml.in + - chainstate:/chainstate + environment: + SIGNER_DB_PATH: /chainstate/stacks-signer-1.sqlite + STACKS_NODE_HOST: stacks-node:20443 + STACKS_SIGNER_ENDPOINT: 0.0.0.0:30001 + SIGNER_PRIVATE_KEY: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 + STACKS_CHAIN_ID: *STACKS_CHAIN_ID + entrypoint: + - /bin/bash + - -c + - | + set -e + envsubst < config.toml.in > config.toml + exec stacks-signer run --config config.toml + + stacks-signer-2: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.stacks-node + args: + GIT_COMMIT: *STACKS_BLOCKCHAIN_COMMIT + depends_on: + - stacks-node + volumes: + - ./signer-0.toml:/root/config.toml.in + - chainstate:/chainstate + environment: + SIGNER_DB_PATH: /chainstate/stacks-signer-2.sqlite + STACKS_NODE_HOST: stacks-node:20443 + STACKS_SIGNER_ENDPOINT: 0.0.0.0:30002 + SIGNER_PRIVATE_KEY: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 + STACKS_CHAIN_ID: *STACKS_CHAIN_ID + entrypoint: + - /bin/bash + - -c + - | + set -e + envsubst < config.toml.in > config.toml + exec stacks-signer run --config config.toml + + stacks-signer-3: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.stacks-node + args: + GIT_COMMIT: *STACKS_BLOCKCHAIN_COMMIT + depends_on: + - stacks-node + volumes: + - ./signer-0.toml:/root/config.toml.in + - chainstate:/chainstate + environment: + SIGNER_DB_PATH: /chainstate/stacks-signer-3.sqlite + STACKS_NODE_HOST: stacks-node:20443 + STACKS_SIGNER_ENDPOINT: 0.0.0.0:30003 + SIGNER_PRIVATE_KEY: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 + STACKS_CHAIN_ID: *STACKS_CHAIN_ID + entrypoint: + - /bin/bash + - -c + - | + set -e + envsubst < config.toml.in > config.toml + exec stacks-signer run --config config.toml + +networks: + stacks: +volumes: + chainstate: diff --git a/tests/pox5/regtest-env/init-data/.gitkeep b/tests/pox5/regtest-env/init-data/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pox5/regtest-env/signer-0.toml b/tests/pox5/regtest-env/signer-0.toml new file mode 100644 index 0000000000..84bde18833 --- /dev/null +++ b/tests/pox5/regtest-env/signer-0.toml @@ -0,0 +1,8 @@ +stacks_private_key = "$SIGNER_PRIVATE_KEY" +node_host = "$STACKS_NODE_HOST" # eg "127.0.0.1:20443" +# must be added as event_observer in node config: +endpoint = "$STACKS_SIGNER_ENDPOINT" # e.g 127.0.0.1:30000 +network = "testnet" +auth_password = "12345" +db_path = "$SIGNER_DB_PATH" +chain_id = $STACKS_CHAIN_ID diff --git a/tests/pox5/regtest-env/stacking/btc-helpers.ts b/tests/pox5/regtest-env/stacking/btc-helpers.ts new file mode 100644 index 0000000000..b24861c71e --- /dev/null +++ b/tests/pox5/regtest-env/stacking/btc-helpers.ts @@ -0,0 +1,130 @@ +import * as BTC from '@scure/btc-signer'; +import { hex } from '@scure/base'; +import { createAddress } from '@stacks/transactions'; + +export const REGTEST_NETWORK = { + bech32: 'bcrt', + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, +} as const; + +// -- Script construction -- + +export function getUnlockBytes(pubKeyHex: string): Uint8Array { + return BTC.Script.encode([hex.decode(pubKeyHex), 'CHECKSIG']); +} + +export function serializeLockupScript({ + stacker, + unlockBurnHeight, + unlockBytes, +}: { + stacker: string; + unlockBurnHeight: bigint; + unlockBytes: Uint8Array; +}): Uint8Array { + const addr = createAddress(stacker); + return BTC.Script.encode([ + new Uint8Array([5, addr.version, ...hex.decode(addr.hash160)]), + 'DROP', + Number(unlockBurnHeight), + 'CHECKLOCKTIMEVERIFY', + 'DROP', + unlockBytes, + ]); +} + +export function toWitnessOutput(script: Uint8Array): Uint8Array { + return BTC.OutScript.encode(BTC.p2wsh({ type: 'wsh', script })); +} + +// -- Unlock height calculation -- + +export function calculateUnlockBurnHeight( + currentCycle: number, + numCycles: number, + rewardCycleLength: number, +): bigint { + const startCycle = currentCycle + 1; + const lastCycle = startCycle + numCycles; + const lastCycleStartHeight = (lastCycle * rewardCycleLength) + 1; + return BigInt(lastCycleStartHeight) + (BigInt(rewardCycleLength) / 2n); +} + +// -- P2WSH address from lock script -- + +export function getLockingAddress(lockScript: Uint8Array): string { + const p2wsh = BTC.p2wsh({ + script: lockScript, + type: 'sh', + }, REGTEST_NETWORK); + return p2wsh.address!; +} + +// -- Bitcoin RPC -- + +const host = process.env.BITCOIN_RPC_HOST ?? 'bitcoind'; +const port = process.env.BITCOIN_RPC_PORT ?? '18443'; +const user = process.env.BITCOIN_RPC_USER ?? 'btc'; +const pass = process.env.BITCOIN_RPC_PASS ?? 'btc'; + +const auth = 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64'); + +export async function bitcoinRPC( + method: string, + params: unknown[] = [], + wallet?: string, +): Promise { + const base = `http://${host}:${port}`; + const url = wallet ? `${base}/wallet/${wallet}` : base; + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: auth }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), + }); + const json: any = await res.json(); + if (json.error) throw new Error(`bitcoinRPC ${method}: ${JSON.stringify(json.error)}`); + return json.result as T; +} + +export async function createOrLoadWallet(name: string) { + try { + await bitcoinRPC('createwallet', [name, false, false, '', false, false, true]); + } catch (e: any) { + if (!e.message.includes('already exists')) throw e; + } +} + +export function getNewAddress(wallet: string) { + return bitcoinRPC('getnewaddress', ['staking'], wallet); +} + +export interface Utxo { + txid: string; + vout: number; + address: string; + amount: number; + confirmations: number; + scriptPubKey: string; +} + +export function listUnspent(wallet: string, minConf = 1) { + return bitcoinRPC('listunspent', [minConf], wallet); +} + +export function getRawTransaction(txid: string): Promise { + return bitcoinRPC('getrawtransaction', [txid, false]); +} + +export function sendRawTransaction(hex: string) { + return bitcoinRPC('sendrawtransaction', [hex]); +} + +export function sendToAddress(wallet: string, address: string, amountBtc: number) { + return bitcoinRPC('sendtoaddress', [address, amountBtc], wallet); +} + +export function getBlockCount() { + return bitcoinRPC('getblockcount'); +} diff --git a/tests/pox5/regtest-env/stacking/btc-staker.ts b/tests/pox5/regtest-env/stacking/btc-staker.ts new file mode 100644 index 0000000000..c669cecafd --- /dev/null +++ b/tests/pox5/regtest-env/stacking/btc-staker.ts @@ -0,0 +1,332 @@ +import { + makeContractCall, + broadcastTransaction, + AnchorMode, + makeContractDeploy, +} from '@stacks/transactions'; +import { hex } from '@scure/base'; +import { PoxInfo } from '@stacks/stacking'; +import { + accounts, + parseEnvInt, + waitForSetup, + logger, + burnBlockToRewardCycle, + network, + POX_REWARD_LENGTH, + type Account, + EPOCH_40_START, + WALLET_NAME, + waitForTxConfirmed, + EPOCH_30_START, + fetchAccount, +} from './common.js'; +import { + getUnlockBytes, + serializeLockupScript, + calculateUnlockBurnHeight, + getLockingAddress, + createOrLoadWallet, + listUnspent, + sendToAddress, +} from './btc-helpers.js'; +import { signSignerKeyGrant, pox5, pox5Signer, clarigenClient } from './pox-5-helpers.js'; +import { readFile } from 'node:fs/promises'; + +const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); +const stakingCyclesPox5 = parseEnvInt('STACKING_CYCLES_POX_5', true); +const lockAmountSats = BigInt(parseEnvInt('BTC_LOCK_AMOUNT_SATS', false) ?? 10_000_000); + +let txFee = parseEnvInt('STACKING_FEE', false) ?? 1_000_000; +const getNextTxFee = () => txFee++; + +// -- Initialization -- + +async function initBtcWallet() { + await createOrLoadWallet(WALLET_NAME); + logger.info({ wallet: WALLET_NAME }, 'Bitcoin staking wallet ready'); + + // Wait for miner to fund the wallet + while (true) { + const utxos = await listUnspent(WALLET_NAME, 1); + const total = utxos.reduce((sum, u) => sum + u.amount, 0); + if (total > 0) { + logger.info({ balance: total }, 'Staking wallet funded'); + return; + } + logger.info('Waiting for staking wallet to be funded...'); + await new Promise(r => setTimeout(r, 5000)); + } +} + +// -- L2: Stacks contract calls -- + +async function submitStake(account: Account, poxInfo: PoxInfo) { + const stakeFnCall = pox5.stake({ + startBurnHt: poxInfo.current_burnchain_block_height!, + amountUstx: 100_000_000000n, + numCycles: stakingCyclesPox5, + signerManager: account.signerManager, + signerCalldata: null, + }); + + const tx = await makeContractCall({ + ...stakeFnCall, + senderKey: account.privKey, + network, + fee: getNextTxFee(), + nonce: (await fetchAccount(account.stxAddress)).nonce, + }); + const result = await broadcastTransaction({ + transaction: tx, + network, + }); + if ('reason' in result) { + account.logger.error( + { + ...result, + }, + `Error staking: ${result.reason}` + ); + throw new Error(`Error staking: ${result.reason}`); + } + account.logger.info({ ...result }, 'stake tx broadcast'); + return result; +} + +async function submitStakeExtend(account: Account) { + const txOptions = { + ...pox5.stakeUpdate({ + amountIncrease: 0n, + cyclesToExtend: stakingCyclesPox5, + signerManager: account.signerManager, + oldSignerManager: account.signerManager, + signerCalldata: null, + }), + senderKey: account.privKey, + network, + fee: getNextTxFee(), + anchorMode: AnchorMode.Any, + }; + + const tx = await makeContractCall(txOptions); + const result = await broadcastTransaction({ + transaction: tx, + network, + }); + if ('reason' in result) { + account.logger.error({ ...result }, `Error extending stake: ${result.reason}`); + throw new Error(`Error extending stake: ${result.reason}`); + } + account.logger.info({ txid: result.txid }, 'L2 stake-extend tx broadcast'); + return result; +} + +// -- L1: Bitcoin locking transaction -- + +async function submitBtcLock(account: Account, unlockBurnHeight: bigint, unlockBytes: Uint8Array) { + const lockScript = serializeLockupScript({ + stacker: account.stxAddress, + unlockBurnHeight, + unlockBytes, + }); + + const address = getLockingAddress(lockScript); + const amountBtc = Number(lockAmountSats) / 1e8; + + const txid = await sendToAddress(WALLET_NAME, address, amountBtc); + account.logger.info( + { txid, address, amountBtc, unlockBurnHeight: unlockBurnHeight.toString() }, + 'L1 BTC lock tx broadcast' + ); + return txid; +} + +// -- Main loop -- + +const grantedSignerKeys = new Set(); +let hasDeployedSBTC = false; + +async function run() { + const poxInfo = await accounts[0]!.client.getPoxInfo(); + + if (poxInfo.current_burnchain_block_height! > EPOCH_30_START + 1 && !hasDeployedSBTC) { + await deploySBTC(accounts[0]!); + hasDeployedSBTC = true; + } + if (poxInfo.current_burnchain_block_height! < EPOCH_40_START) { + // logger.info({ burnHeight: poxInfo.current_burnchain_block_height }, 'Not on epoch 3.5 yet, skipping'); + return; + } + + const currentCycle = poxInfo.reward_cycle_id; + + const accountInfos = await Promise.all( + accounts.map(async a => { + const info = await fetchAccount(a.stxAddress); + return { ...a, ...info }; + }) + ); + + const nowCycle = burnBlockToRewardCycle(poxInfo.current_burnchain_block_height ?? 0); + + const txIdsToWait: string[] = []; + + for (const account of accountInfos) { + const unlockBytes = getUnlockBytes(account.pubKey); + const unlockBurnHeight = calculateUnlockBurnHeight( + currentCycle, + stakingCyclesPox5, + POX_REWARD_LENGTH + ); + + if (!grantedSignerKeys.has(account.signerManager)) { + const authId = 2n; + const signature = signSignerKeyGrant({ + signerManager: account.signerManager, + authId, + signerSk: hex.decode(account.signerPrivKey), + }); + + const signerManager = await readFile('./contracts/pox-5-signer.clar', 'utf8'); + const deployTx = await makeContractDeploy({ + senderKey: account.privKey, + network, + contractName: 'signer-manager', + codeBody: signerManager + .replaceAll(' .pox-5', ` '${pox5.identifier}`) + .replaceAll( + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4', + 'ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP' + ), + }); + const deployResult = await broadcastTransaction({ + transaction: deployTx, + network, + }); + const exists = 'reason' in deployResult && deployResult.reason === 'ContractAlreadyExists'; + if (!exists) { + if ('reason' in deployResult) { + throw new Error(`Error deploying signer manager: ${deployResult.reason}`); + } + account.logger.info({ ...deployResult }, 'Deployed signer manager'); + await waitForTxConfirmed(deployResult.txid); + } + + const signerKey = await clarigenClient.ro(pox5.getSignerInfo(account.signerManager)); + + if (!signerKey) { + const registerSelf = await makeContractCall({ + ...pox5Signer(account.signerManager).registerSelf({ + signerManager: account.signerManager, + signerKey: hex.decode(account.signerPubKey), + authId, + signerSig: signature, + }), + nonce: (await fetchAccount(account.stxAddress)).nonce, + senderKey: account.privKey, + network, + }); + const registerSelfResult = await broadcastTransaction({ + transaction: registerSelf, + network, + }); + if ('reason' in registerSelfResult) { + throw new Error(`Error registering signer manager: ${registerSelfResult.reason}`); + } + account.logger.info({ ...registerSelfResult }, 'Registered self'); + await waitForTxConfirmed(registerSelfResult.txid); + } + grantedSignerKeys.add(account.signerManager); + } + + if (account.lockedAmount === 0n) { + account.logger.info('Account unlocked, staking...', { + account: account.index, + rewardCycle: poxInfo.reward_cycle_id, + unlockBurnHeight: unlockBurnHeight.toString(), + }); + + const stakeResult = await submitStake(account, poxInfo); + txIdsToWait.push(stakeResult.txid); + + await submitBtcLock(account, unlockBurnHeight, unlockBytes); + continue; + } + + const unlockCycle = burnBlockToRewardCycle(account.unlockHeight); + + if (unlockCycle === nowCycle) { + account.logger.info( + { unlockHeight: account.unlockHeight, nowCycle, unlockCycle }, + 'Extending stake...' + ); + + const stakeExtendResult = await submitStakeExtend(account); + txIdsToWait.push(stakeExtendResult.txid); + + await submitBtcLock(account, unlockBurnHeight, unlockBytes); + continue; + } + + // account.logger.info({ nowCycle, unlockCycle }, 'Staked through next cycle, skipping'); + } + await Promise.all(txIdsToWait.map(waitForTxConfirmed)); +} + +async function deploySBTC(account: Account) { + const registry = await readFile( + 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar', + 'utf8' + ); + const token = await readFile( + 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar', + 'utf8' + ); + const withdrawal = await readFile( + 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar', + 'utf8' + ); + + async function deployContract(contract: string, name: string) { + const deployTx = await makeContractDeploy({ + senderKey: accounts[0]!.privKey, + network, + contractName: name, + codeBody: contract, + clarityVersion: 3, + }); + const deployResult = await broadcastTransaction({ + transaction: deployTx, + network, + }); + if ('reason' in deployResult) { + if (deployResult.reason === 'ContractAlreadyExists') { + return; + } + throw new Error(`Error deploying sbtc contract: ${deployResult.reason}`); + } + account.logger.info({ ...deployResult, contractName: name }, 'Deployed sbtc contract'); + await waitForTxConfirmed(deployResult.txid); + } + + await deployContract(registry, 'sbtc-registry'); + await deployContract(token, 'sbtc-token'); + await deployContract(withdrawal, 'sbtc-withdrawal'); +} + +async function loop() { + await waitForSetup(); + await initBtcWallet(); + + while (true) { + try { + await run(); + } catch (e) { + logger.error(e, 'Error in btc-staker loop'); + } + await new Promise(r => setTimeout(r, stakingInterval * 1000)); + } +} + +loop(); diff --git a/tests/pox5/regtest-env/stacking/clarigen-types.ts b/tests/pox5/regtest-env/stacking/clarigen-types.ts new file mode 100644 index 0000000000..807410fdab --- /dev/null +++ b/tests/pox5/regtest-env/stacking/clarigen-types.ts @@ -0,0 +1,1733 @@ + +import type { TypedAbiArg, TypedAbiFunction, TypedAbiMap, TypedAbiVariable, Response } from '@clarigen/core'; + +export const contracts = { + pox5: { + "functions": { + addSignerToSetForCycle: {"name":"add-signer-to-set-for-cycle","access":"private","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], Response>, + addStakerToBond: {"name":"add-staker-to-bond","access":"private","args":[{"name":"staker-item","type":{"tuple":[{"name":"max-sats","type":"uint128"},{"name":"staker","type":"principal"}]}},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"sum-max-sats","type":"uint128"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"sum-max-sats","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[stakerItem: TypedAbiArg<{ + "maxSats": number | bigint; + "staker": string; +}, "stakerItem">, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "bondIndex": bigint; + "sumMaxSats": bigint; +}, bigint>>, + addStakerToSignerCycles: {"name":"add-staker-to-signer-cycles","access":"private","args":[{"name":"staker","type":"principal"},{"name":"signer","type":"principal"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"amount-ustx","type":"uint128"},{"name":"is-stx-staking","type":"bool"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, signer: TypedAbiArg, firstRewardCycle: TypedAbiArg, numCycles: TypedAbiArg, amountUstx: TypedAbiArg, isStxStaking: TypedAbiArg], Response<{ + "amountUstx": bigint; + "firstRewardCycle": bigint; + "isStxStaking": boolean; + "signer": string; + "staker": string; +}, bigint>>, + addStakerToSignerForCycle: {"name":"add-staker-to-signer-for-cycle","access":"private","args":[{"name":"cycle-index","type":"uint128"},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[cycleIndex: TypedAbiArg, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "amountUstx": bigint; + "firstRewardCycle": bigint; + "isStxStaking": boolean; + "signer": string; + "staker": string; +}, bigint>>, + assertActiveBondIncluded: {"name":"assert-active-bond-included","access":"private","args":[{"name":"offset","type":"uint128"},{"name":"acc-res","type":{"response":{"ok":{"tuple":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"calculation-height","type":"uint128"},{"name":"latest-bond-index","type":"uint128"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"calculation-height","type":"uint128"},{"name":"latest-bond-index","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[offset: TypedAbiArg, accRes: TypedAbiArg, "accRes">], Response<{ + "bondPeriods": bigint[]; + "calculationHeight": bigint; + "latestBondIndex": bigint; +}, bigint>>, + calculateBondRewards: {"name":"calculate-bond-rewards","access":"private","args":[{"name":"bond-index","type":"uint128"},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"available-rewards","type":"uint128"},{"name":"calculation-height","type":"uint128"},{"name":"last-bond-index","type":{"optional":"uint128"}},{"name":"last-bond-stx-value-ratio","type":{"optional":"uint128"}}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"available-rewards","type":"uint128"},{"name":"calculation-height","type":"uint128"},{"name":"last-bond-index","type":{"optional":"uint128"}},{"name":"last-bond-stx-value-ratio","type":{"optional":"uint128"}}]},"error":"uint128"}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "availableRewards": bigint; + "calculationHeight": bigint; + "lastBondIndex": bigint | null; + "lastBondStxValueRatio": bigint | null; +}, bigint>>, + lockSbtc: {"name":"lock-sbtc","access":"private","args":[{"name":"amount","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg], Response>, + matchUintInList: {"name":"match-uint-in-list","access":"private","args":[{"name":"item","type":"uint128"},{"name":"acc","type":{"tuple":[{"name":"found","type":"bool"},{"name":"needle","type":"uint128"}]}}],"outputs":{"type":{"tuple":[{"name":"found","type":"bool"},{"name":"needle","type":"uint128"}]}}} as TypedAbiFunction<[item: TypedAbiArg, acc: TypedAbiArg<{ + "found": boolean; + "needle": number | bigint; +}, "acc">], { + "found": boolean; + "needle": bigint; +}>, + removeStakerFromCycles: {"name":"remove-staker-from-cycles","access":"private","args":[{"name":"staker","type":"principal"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"is-stx-staking","type":"bool"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstRewardCycle: TypedAbiArg, numCycles: TypedAbiArg, isStxStaking: TypedAbiArg], Response<{ + "firstRewardCycle": bigint; + "isStxStaking": boolean; + "staker": string; +}, bigint>>, + removeStakerFromSetForCycle: {"name":"remove-staker-from-set-for-cycle","access":"private","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], Response>, + removeStakerFromSignerForCycle: {"name":"remove-staker-from-signer-for-cycle","access":"private","args":[{"name":"cycle-index","type":"uint128"},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[cycleIndex: TypedAbiArg, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "firstRewardCycle": bigint; + "isStxStaking": boolean; + "staker": string; +}, bigint>>, + reverseBuff16: {"name":"reverse-buff16","access":"private","args":[{"name":"input","type":{"buffer":{"length":16}}}],"outputs":{"type":{"buffer":{"length":17}}}} as TypedAbiFunction<[input: TypedAbiArg], Uint8Array>, + settleRewards: {"name":"settle-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], { + "earned": bigint; + "rewardsPerToken": bigint; +}>, + updateClaimableBondRewards: {"name":"update-claimable-bond-rewards","access":"private","args":[{"name":"bond-index","type":"uint128"},{"name":"accumulator","type":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"signer","type":"principal"},{"name":"total","type":"uint128"}]}}],"outputs":{"type":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"signer","type":"principal"},{"name":"total","type":"uint128"}]}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, accumulator: TypedAbiArg<{ + "bondRewards": { + "bondIndex": number | bigint; + "earned": number | bigint; + "rewardsPerToken": number | bigint; +}[]; + "signer": string; + "total": number | bigint; +}, "accumulator">], { + "bondRewards": { + "bondIndex": bigint; + "earned": bigint; + "rewardsPerToken": bigint; +}[]; + "signer": string; + "total": bigint; +}>, + updateClaimableRewards: {"name":"update-claimable-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], { + "earned": bigint; + "rewardsPerToken": bigint; +}>, + validateL1Lockup: {"name":"validate-l1-lockup","access":"private","args":[{"name":"lockup","type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"header","type":{"buffer":{"length":80}}},{"name":"height","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}},{"name":"output-index","type":"uint128"},{"name":"tx","type":{"buffer":{"length":100000}}},{"name":"tx-count","type":"uint128"},{"name":"tx-index","type":"uint128"}]}},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"expected-script-hash","type":{"buffer":{"length":34}}},{"name":"sum","type":"uint128"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"expected-script-hash","type":{"buffer":{"length":34}}},{"name":"sum","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[lockup: TypedAbiArg<{ + "amount": number | bigint; + "header": Uint8Array; + "height": number | bigint; + "leafHashes": Uint8Array[]; + "outputIndex": number | bigint; + "tx": Uint8Array; + "txCount": number | bigint; + "txIndex": number | bigint; +}, "lockup">, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "expectedScriptHash": Uint8Array; + "sum": bigint; +}, bigint>>, + verifyL1Lockups: {"name":"verify-l1-lockups","access":"private","args":[{"name":"staker","type":"principal"},{"name":"bond-index","type":"uint128"},{"name":"lockups","type":{"tuple":[{"name":"outputs","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"header","type":{"buffer":{"length":80}}},{"name":"height","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}},{"name":"output-index","type":"uint128"},{"name":"tx","type":{"buffer":{"length":100000}}},{"name":"tx-count","type":"uint128"},{"name":"tx-index","type":"uint128"}]},"length":10}}},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}]}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, bondIndex: TypedAbiArg, lockups: TypedAbiArg<{ + "outputs": { + "amount": number | bigint; + "header": Uint8Array; + "height": number | bigint; + "leafHashes": Uint8Array[]; + "outputIndex": number | bigint; + "tx": Uint8Array; + "txCount": number | bigint; + "txIndex": number | bigint; +}[]; + "unlockBytes": Uint8Array; +}, "lockups">], Response>, + allowContractCaller: {"name":"allow-contract-caller","access":"public","args":[{"name":"caller","type":"principal"},{"name":"until-burn-ht","type":{"optional":"uint128"}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[caller: TypedAbiArg, untilBurnHt: TypedAbiArg], Response>, + announceL1EarlyExit: {"name":"announce-l1-early-exit","access":"public","args":[{"name":"staker","type":"principal"},{"name":"old-signer-manager","type":"trait_reference"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, oldSignerManager: TypedAbiArg], Response>, + calculateRewards: {"name":"calculate-rewards","access":"public","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg], Response>, + claimRewards: {"name":"claim-rewards","access":"public","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"bond-totals","type":"uint128"},{"name":"stx-rewards","type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}},{"name":"total-rewards","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg, rewardCycle: TypedAbiArg], Response<{ + "bondRewards": { + "bondIndex": bigint; + "earned": bigint; + "rewardsPerToken": bigint; +}[]; + "bondTotals": bigint; + "stxRewards": { + "earned": bigint; + "rewardsPerToken": bigint; +}; + "totalRewards": bigint; +}, bigint>>, + disallowContractCaller: {"name":"disallow-contract-caller","access":"public","args":[{"name":"caller","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[caller: TypedAbiArg], Response>, + grantSignerKey: {"name":"grant-signer-key","access":"public","args":[{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-manager","type":"principal"},{"name":"auth-id","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerKey: TypedAbiArg, signerManager: TypedAbiArg, authId: TypedAbiArg, signerSig: TypedAbiArg], Response>, + registerForBond: {"name":"register-for-bond","access":"public","args":[{"name":"bond-index","type":"uint128"},{"name":"signer-manager","type":"trait_reference"},{"name":"amount-ustx","type":"uint128"},{"name":"btc-lockup","type":{"response":{"ok":{"tuple":[{"name":"outputs","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"header","type":{"buffer":{"length":80}}},{"name":"height","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}},{"name":"output-index","type":"uint128"},{"name":"tx","type":{"buffer":{"length":100000}}},{"name":"tx-count","type":"uint128"},{"name":"tx-index","type":"uint128"}]},"length":10}}},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}]},"error":"uint128"}}},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"bond-index","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, signerManager: TypedAbiArg, amountUstx: TypedAbiArg, btcLockup: TypedAbiArg, "btcLockup">, signerCalldata: TypedAbiArg], Response<{ + "amountUstx": bigint; + "bondIndex": bigint; + "firstRewardCycle": bigint; + "signer": string; + "staker": string; + "unlockBurnHeight": bigint; + "unlockCycle": bigint; +}, bigint>>, + registerSigner: {"name":"register-signer","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"signer","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg], Response<{ + "signer": string; + "signerKey": Uint8Array; +}, bigint>>, + revokeSignerGrant: {"name":"revoke-signer-grant","access":"public","args":[{"name":"signer-manager","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg], Response>, + setBondAdmin: {"name":"set-bond-admin","access":"public","args":[{"name":"new-admin","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newAdmin: TypedAbiArg], Response>, + setBurnchainParameters: {"name":"set-burnchain-parameters","access":"public","args":[{"name":"first-burn-height","type":"uint128"},{"name":"prepare-cycle-length","type":"uint128"},{"name":"reward-cycle-length","type":"uint128"},{"name":"begin-pox5-reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[firstBurnHeight: TypedAbiArg, prepareCycleLength: TypedAbiArg, rewardCycleLength: TypedAbiArg, beginPox5RewardCycle: TypedAbiArg], Response>, + setupBond: {"name":"setup-bond","access":"public","args":[{"name":"bond-index","type":"uint128"},{"name":"target-rate","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-admin","type":"principal"},{"name":"allowlist","type":{"list":{"type":{"tuple":[{"name":"max-sats","type":"uint128"},{"name":"staker","type":"principal"}]},"length":1000}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}},{"name":"max-allocation-sats","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, targetRate: TypedAbiArg, stxValueRatio: TypedAbiArg, minUstxRatio: TypedAbiArg, earlyUnlockBytes: TypedAbiArg, earlyUnlockAdmin: TypedAbiArg, allowlist: TypedAbiArg<{ + "maxSats": number | bigint; + "staker": string; +}[], "allowlist">], Response<{ + "bondIndex": bigint; + "earlyUnlockBytes": Uint8Array; + "maxAllocationSats": bigint; + "minUstxRatio": bigint; + "stxValueRatio": bigint; + "targetRate": bigint; +}, bigint>>, + stake: {"name":"stake","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"start-burn-ht","type":"uint128"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycle","type":"uint128"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, amountUstx: TypedAbiArg, numCycles: TypedAbiArg, startBurnHt: TypedAbiArg, signerCalldata: TypedAbiArg], Response<{ + "amountUstx": bigint; + "firstRewardCycle": bigint; + "numCycle": bigint; + "signer": string; + "staker": string; + "unlockBurnHeight": bigint; + "unlockCycle": bigint; +}, bigint>>, + stakeUpdate: {"name":"stake-update","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"old-signer-manager","type":"trait_reference"},{"name":"cycles-to-extend","type":"uint128"},{"name":"amount-increase","type":"uint128"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"prev-unlock-height","type":"uint128"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, oldSignerManager: TypedAbiArg, cyclesToExtend: TypedAbiArg, amountIncrease: TypedAbiArg, signerCalldata: TypedAbiArg], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "prevUnlockHeight": bigint; + "signer": string; + "staker": string; + "unlockBurnHeight": bigint; + "unlockCycle": bigint; +}, bigint>>, + unstake: {"name":"unstake","access":"public","args":[{"name":"old-signer-manager","type":"trait_reference"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[oldSignerManager: TypedAbiArg], Response<{ + "amountUstx": bigint; + "firstRewardCycle": bigint; + "staker": string; + "unlockBurnHeight": bigint; + "unlockCycle": bigint; +}, bigint>>, + unstakeSbtc: {"name":"unstake-sbtc","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"amount-to-withdrawal-sats","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"new-amount-sats","type":"uint128"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, amountToWithdrawalSats: TypedAbiArg], Response<{ + "newAmountSats": bigint; + "signer": string; + "staker": string; +}, bigint>>, + updateBondRegistration: {"name":"update-bond-registration","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"old-signer-manager","type":"trait_reference"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, oldSignerManager: TypedAbiArg, signerCalldata: TypedAbiArg], Response>, + assertAllActiveBondsIncluded: {"name":"assert-all-active-bonds-included","access":"read_only","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"calculation-height","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg, calculationHeight: TypedAbiArg], Response>, + bondPeriodToBurnHeight: {"name":"bond-period-to-burn-height","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, + bondPeriodToRewardCycle: {"name":"bond-period-to-reward-cycle","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, + burnHeightToDistributionIndex: {"name":"burn-height-to-distribution-index","access":"read_only","args":[{"name":"height","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[height: TypedAbiArg], bigint>, + burnHeightToRewardCycle: {"name":"burn-height-to-reward-cycle","access":"read_only","args":[{"name":"height","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[height: TypedAbiArg], bigint>, + checkCallerAllowed: {"name":"check-caller-allowed","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[], Response>, + checkPoxLockPeriod: {"name":"check-pox-lock-period","access":"read_only","args":[{"name":"lock-period","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[lockPeriod: TypedAbiArg], boolean>, + constructLockupOutputScript: {"name":"construct-lockup-output-script","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"buffer":{"length":34}}}} as TypedAbiFunction<[staker: TypedAbiArg, unlockBurnHeight: TypedAbiArg, unlockBytes: TypedAbiArg, earlyUnlockBytes: TypedAbiArg], Uint8Array>, + constructLockupScript: {"name":"construct-lockup-script","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"buffer":{"length":4109}}}} as TypedAbiFunction<[staker: TypedAbiArg, unlockBurnHeight: TypedAbiArg, unlockBytes: TypedAbiArg, earlyUnlockBytes: TypedAbiArg], Uint8Array>, + currentDistributionCycle: {"name":"current-distribution-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + currentPoxRewardCycle: {"name":"current-pox-reward-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + distributionCycleToBurnHeight: {"name":"distribution-cycle-to-burn-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, + getAmountDelegatedForSigner: {"name":"get-amount-delegated-for-signer","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], bigint>, + getBcHHash: {"name":"get-bc-h-hash","access":"read_only","args":[{"name":"bh","type":"uint128"}],"outputs":{"type":{"optional":{"buffer":{"length":32}}}}} as TypedAbiFunction<[bh: TypedAbiArg], Uint8Array | null>, + getBondAllowance: {"name":"get-bond-allowance","access":"read_only","args":[{"name":"bond-index","type":"uint128"},{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":"uint128"}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, staker: TypedAbiArg], bigint | null>, + getBondL1UnlockHeight: {"name":"get-bond-l1-unlock-height","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, + getBondMembership: {"name":"get-bond-membership","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"bond-index","type":"uint128"},{"name":"is-l1-lock","type":"bool"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg], { + "amountUstx": bigint; + "bondIndex": bigint; + "isL1Lock": boolean; + "signer": string; +} | null>, + getEarned: {"name":"get-earned","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getFirstPox5RewardCycle: {"name":"get-first-pox-5-reward-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getLastAccountedRewardsOnly: {"name":"get-last-accounted-rewards-only","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getLastRewardComputeHeight: {"name":"get-last-reward-compute-height","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getNewRewards: {"name":"get-new-rewards","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getPoxInfo: {"name":"get-pox-info","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-burnchain-block-height","type":"uint128"},{"name":"min-amount-ustx","type":"uint128"},{"name":"prepare-cycle-length","type":"uint128"},{"name":"reward-cycle-id","type":"uint128"},{"name":"reward-cycle-length","type":"uint128"},{"name":"total-liquid-supply-ustx","type":"uint128"}]},"error":"none"}}}} as TypedAbiFunction<[], Response<{ + "firstBurnchainBlockHeight": bigint; + "minAmountUstx": bigint; + "prepareCycleLength": bigint; + "rewardCycleId": bigint; + "rewardCycleLength": bigint; + "totalLiquidSupplyUstx": bigint; +}, null>>, + getProtocolBond: {"name":"get-protocol-bond","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"early-unlock-admin","type":"principal"},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg], { + "earlyUnlockAdmin": string; + "earlyUnlockBytes": Uint8Array; + "minUstxRatio": bigint; + "stxValueRatio": bigint; + "targetRate": bigint; +} | null>, + getReserveBalance: {"name":"get-reserve-balance","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getReversedTxid: {"name":"get-reversed-txid","access":"read_only","args":[{"name":"tx","type":{"buffer":{"length":100000}}}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[tx: TypedAbiArg], Uint8Array>, + getRewards: {"name":"get-rewards","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getSignerCycleMembership: {"name":"get-signer-cycle-membership","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], { + "amountUstx": bigint; + "signer": string; +} | null>, + getSignerGrantMessageHash: {"name":"get-signer-grant-message-hash","access":"read_only","args":[{"name":"signer-manager","type":"principal"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, authId: TypedAbiArg], Uint8Array>, + getSignerInfo: {"name":"get-signer-info","access":"read_only","args":[{"name":"signer","type":"principal"}],"outputs":{"type":{"optional":{"buffer":{"length":33}}}}} as TypedAbiFunction<[signer: TypedAbiArg], Uint8Array | null>, + getSignerPendingStakedUstxPerCycle: {"name":"get-signer-pending-staked-ustx-per-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], bigint>, + getSignerRewardsPerTokenSettledForCycle: {"name":"get-signer-rewards-per-token-settled-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getSignerSetFirstItemForCycle: {"name":"get-signer-set-first-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, + getSignerSetItemForCycle: {"name":"get-signer-set-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], { + "next": string | null; + "prev": string | null; +} | null>, + getSignerSetLastItemForCycle: {"name":"get-signer-set-last-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, + getSignerSetNextItemForCycle: {"name":"get-signer-set-next-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], string | null>, + getSignerSetPrevItemForCycle: {"name":"get-signer-set-prev-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], string | null>, + getSignerSharesStakedForCycle: {"name":"get-signer-shares-staked-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getSignerUnclaimedRewardsForCycle: {"name":"get-signer-unclaimed-rewards-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getStakerInfo: {"name":"get-staker-info","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg], { + "amountUstx": bigint; + "firstRewardCycle": bigint; + "numCycles": bigint; + "signer": string; +} | null>, + getStakerSharesStakedForCycle: {"name":"get-staker-shares-staked-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"},{"name":"signer","type":"principal"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg, signer: TypedAbiArg], bigint>, + getTotalSbtcStaked: {"name":"get-total-sbtc-staked","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getTotalSbtcStakedForBond: {"name":"get-total-sbtc-staked-for-bond","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, + getTotalSharesStakedForCycle: {"name":"get-total-shares-staked-for-cycle","access":"read_only","args":[{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getTotalUstxStacked: {"name":"get-total-ustx-stacked","access":"read_only","args":[{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[rewardCycle: TypedAbiArg], bigint>, + getUstxDelegatedForCycle: {"name":"get-ustx-delegated-for-cycle","access":"read_only","args":[{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[rewardCycle: TypedAbiArg], bigint>, + isBondActiveAtHeight: {"name":"is-bond-active-at-height","access":"read_only","args":[{"name":"bond-index","type":"uint128"},{"name":"calculation-height","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[bondIndex: TypedAbiArg, calculationHeight: TypedAbiArg], boolean>, + isInPreparePhase: {"name":"is-in-prepare-phase","access":"read_only","args":[{"name":"current-cycle","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[currentCycle: TypedAbiArg], boolean>, + minUstxForSatsAmount: {"name":"min-ustx-for-sats-amount","access":"read_only","args":[{"name":"sats-amount","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[satsAmount: TypedAbiArg, stxValueRatio: TypedAbiArg, minUstxRatio: TypedAbiArg], bigint>, + parseBlockHeader: {"name":"parse-block-header","access":"read_only","args":[{"name":"headerbuff","type":{"buffer":{"length":80}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"merkle-root","type":{"buffer":{"length":32}}},{"name":"nbits","type":"uint128"},{"name":"nonce","type":"uint128"},{"name":"parent","type":{"buffer":{"length":32}}},{"name":"timestamp","type":"uint128"},{"name":"version","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[headerbuff: TypedAbiArg], Response<{ + "merkleRoot": Uint8Array; + "nbits": bigint; + "nonce": bigint; + "parent": Uint8Array; + "timestamp": bigint; + "version": bigint; +}, bigint>>, + pushCScriptNum: {"name":"push-c-script-num","access":"read_only","args":[{"name":"n","type":"uint128"}],"outputs":{"type":{"buffer":{"length":1027}}}} as TypedAbiFunction<[n: TypedAbiArg], Uint8Array>, + pushScriptBytes: {"name":"push-script-bytes","access":"read_only","args":[{"name":"bytes","type":{"buffer":{"length":1024}}}],"outputs":{"type":{"buffer":{"length":1027}}}} as TypedAbiFunction<[bytes: TypedAbiArg], Uint8Array>, + readHashslice: {"name":"read-hashslice","access":"read_only","args":[{"name":"old-ctx","type":{"tuple":[{"name":"index","type":"uint128"},{"name":"txbuff","type":{"buffer":{"length":4096}}}]}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"ctx","type":{"tuple":[{"name":"index","type":"uint128"},{"name":"txbuff","type":{"buffer":{"length":4096}}}]}},{"name":"hashslice","type":{"buffer":{"length":32}}}]},"error":"uint128"}}}} as TypedAbiFunction<[oldCtx: TypedAbiArg<{ + "index": number | bigint; + "txbuff": Uint8Array; +}, "oldCtx">], Response<{ + "ctx": { + "index": bigint; + "txbuff": Uint8Array; +}; + "hashslice": Uint8Array; +}, bigint>>, + readUint32: {"name":"read-uint32","access":"read_only","args":[{"name":"ctx","type":{"tuple":[{"name":"index","type":"uint128"},{"name":"txbuff","type":{"buffer":{"length":4096}}}]}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"ctx","type":{"tuple":[{"name":"index","type":"uint128"},{"name":"txbuff","type":{"buffer":{"length":4096}}}]}},{"name":"uint32","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[ctx: TypedAbiArg<{ + "index": number | bigint; + "txbuff": Uint8Array; +}, "ctx">], Response<{ + "ctx": { + "index": bigint; + "txbuff": Uint8Array; +}; + "uint32": bigint; +}, bigint>>, + reverseBuff32: {"name":"reverse-buff32","access":"read_only","args":[{"name":"input","type":{"buffer":{"length":32}}}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[input: TypedAbiArg], Uint8Array>, + rewardCycleToBurnHeight: {"name":"reward-cycle-to-burn-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, + rewardCycleToUnlockHeight: {"name":"reward-cycle-to-unlock-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, + serializeCScriptNum: {"name":"serialize-c-script-num","access":"read_only","args":[{"name":"n","type":"uint128"}],"outputs":{"type":{"buffer":{"length":5}}}} as TypedAbiFunction<[n: TypedAbiArg], Uint8Array>, + signerSetContainsForCycle: {"name":"signer-set-contains-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], boolean>, + uintToBuffLe: {"name":"uint-to-buff-le","access":"read_only","args":[{"name":"n","type":"uint128"}],"outputs":{"type":{"buffer":{"length":2}}}} as TypedAbiFunction<[n: TypedAbiArg], Uint8Array>, + verifyBlockHeader: {"name":"verify-block-header","access":"read_only","args":[{"name":"headerbuff","type":{"buffer":{"length":80}}},{"name":"expected-block-height","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[headerbuff: TypedAbiArg, expectedBlockHeight: TypedAbiArg], boolean>, + verifySignerKeyGrant: {"name":"verify-signer-key-grant","access":"read_only","args":[{"name":"signer-manager","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg], Response> + }, + "maps": { + allowanceContractCallers: {"name":"allowance-contract-callers","key":{"tuple":[{"name":"contract-caller","type":"principal"},{"name":"sender","type":"principal"}]},"value":{"optional":"uint128"}} as TypedAbiMap<{ + "contractCaller": string; + "sender": string; +}, bigint | null>, + protocolBondAllowances: {"name":"protocol-bond-allowances","key":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "bondIndex": number | bigint; + "staker": string; +}, bigint>, + protocolBondMemberships: {"name":"protocol-bond-memberships","key":"principal","value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"bond-index","type":"uint128"},{"name":"is-l1-lock","type":"bool"},{"name":"signer","type":"principal"}]}} as TypedAbiMap, + protocolBonds: {"name":"protocol-bonds","key":"uint128","value":{"tuple":[{"name":"early-unlock-admin","type":"principal"},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]}} as TypedAbiMap, + protocolBondsTotalStaked: {"name":"protocol-bonds-total-staked","key":"uint128","value":"uint128"} as TypedAbiMap, + rewardsPerTokenForCycle: {"name":"rewards-per-token-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; +}, bigint>, + signerDelegatedPerCycle: {"name":"signer-delegated-per-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "cycle": number | bigint; + "signer": string; +}, bigint>, + signerKeyGrants: {"name":"signer-key-grants","key":{"tuple":[{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-manager","type":"principal"}]},"value":"bool"} as TypedAbiMap<{ + "signerKey": Uint8Array; + "signerManager": string; +}, boolean>, + signerPendingStakedUstxPerCycle: {"name":"signer-pending-staked-ustx-per-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "cycle": number | bigint; + "signer": string; +}, bigint>, + signerRewardsPerTokenSettledForCycle: {"name":"signer-rewards-per-token-settled-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "signer": string; +}, bigint>, + signerSetLlFirstForCycle: {"name":"signer-set-ll-first-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, + signerSetLlForCycle: {"name":"signer-set-ll-for-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"signer","type":"principal"}]},"value":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}} as TypedAbiMap<{ + "cycle": number | bigint; + "signer": string; +}, { + "next": string | null; + "prev": string | null; +}>, + signerSetLlLastForCycle: {"name":"signer-set-ll-last-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, + signerSharesStakedForCycle: {"name":"signer-shares-staked-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "signer": string; +}, bigint>, + signerUnclaimedRewardsForCycle: {"name":"signer-unclaimed-rewards-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "signer": string; +}, bigint>, + signers: {"name":"signers","key":"principal","value":{"buffer":{"length":33}}} as TypedAbiMap, + stakerInfo: {"name":"staker-info","key":"principal","value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"signer","type":"principal"}]}} as TypedAbiMap, + stakerSharesStakedForCycle: {"name":"staker-shares-staked-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "signer": string; + "staker": string; +}, bigint>, + stakerSignerCycleMemberships: {"name":"staker-signer-cycle-memberships","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"signer","type":"principal"}]}} as TypedAbiMap<{ + "cycle": number | bigint; + "staker": string; +}, { + "amountUstx": bigint; + "signer": string; +}>, + totalSharesStakedForCycle: {"name":"total-shares-staked-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; +}, bigint>, + usedSignerKeyGrants: {"name":"used-signer-key-grants","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-manager","type":"principal"}]},"value":"bool"} as TypedAbiMap<{ + "authId": number | bigint; + "signerKey": Uint8Array; + "signerManager": string; +}, boolean>, + ustxDelegatedPerCycle: {"name":"ustx-delegated-per-cycle","key":"uint128","value":"uint128"} as TypedAbiMap + }, + "variables": { + BOND_GAP_CYCLES: { + name: 'BOND_GAP_CYCLES', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + BOND_LENGTH_CYCLES: { + name: 'BOND_LENGTH_CYCLES', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + ERR_ACTIVE_BOND_NOT_INCLUDED: { + name: 'ERR_ACTIVE_BOND_NOT_INCLUDED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_ALREADY_REGISTERED: { + name: 'ERR_ALREADY_REGISTERED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_ALREADY_STAKED: { + name: 'ERR_ALREADY_STAKED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_BOND_ALREADY_SETUP: { + name: 'ERR_BOND_ALREADY_SETUP', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_BOND_ALREADY_STARTED: { + name: 'ERR_BOND_ALREADY_STARTED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_BOND_NOT_ACTIVE: { + name: 'ERR_BOND_NOT_ACTIVE', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_BOND_NOT_FOUND: { + name: 'ERR_BOND_NOT_FOUND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + eRR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK: { + name: 'ERR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_CANNOT_SETUP_BOND_TOO_LATE: { + name: 'ERR_CANNOT_SETUP_BOND_TOO_LATE', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_CANNOT_SETUP_BOND_TOO_SOON: { + name: 'ERR_CANNOT_SETUP_BOND_TOO_SOON', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_CANNOT_UNSTAKE_SBTC: { + name: 'ERR_CANNOT_UNSTAKE_SBTC', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_DISTRIBUTION_ALREADY_COMPUTED: { + name: 'ERR_DISTRIBUTION_ALREADY_COMPUTED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INSUFFICIENT_STX: { + name: 'ERR_INSUFFICIENT_STX', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_BOND_PERIOD_ORDERING: { + name: 'ERR_INVALID_BOND_PERIOD_ORDERING', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_BTC_HEADER: { + name: 'ERR_INVALID_BTC_HEADER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_LOCKUP_AMOUNT: { + name: 'ERR_INVALID_LOCKUP_AMOUNT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_LOCKUP_SCRIPT: { + name: 'ERR_INVALID_LOCKUP_SCRIPT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_MERKLE_PROOF: { + name: 'ERR_INVALID_MERKLE_PROOF', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_NUM_CYCLES: { + name: 'ERR_INVALID_NUM_CYCLES', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_OLD_SIGNER_MANAGER: { + name: 'ERR_INVALID_OLD_SIGNER_MANAGER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_SIGNATURE_PUBKEY: { + name: 'ERR_INVALID_SIGNATURE_PUBKEY', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_SIGNATURE_RECOVER: { + name: 'ERR_INVALID_SIGNATURE_RECOVER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_START_BURN_HEIGHT: { + name: 'ERR_INVALID_START_BURN_HEIGHT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_UNSTAKE_SBTC_AMOUNT: { + name: 'ERR_INVALID_UNSTAKE_SBTC_AMOUNT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NOT_ALLOWLISTED: { + name: 'ERR_NOT_ALLOWLISTED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NOT_BOND_PARTICIPANT: { + name: 'ERR_NOT_BOND_PARTICIPANT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NOT_STAKING: { + name: 'ERR_NOT_STAKING', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NO_CLAIMABLE_REWARDS: { + name: 'ERR_NO_CLAIMABLE_REWARDS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_READ_TX_OUT_OF_BOUNDS: { + name: 'ERR_READ_TX_OUT_OF_BOUNDS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_KEY_GRANT_NOT_FOUND: { + name: 'ERR_SIGNER_KEY_GRANT_NOT_FOUND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_KEY_GRANT_USED: { + name: 'ERR_SIGNER_KEY_GRANT_USED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_NOT_FOUND: { + name: 'ERR_SIGNER_NOT_FOUND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_STAKER_ALREADY_ADDED: { + name: 'ERR_STAKER_ALREADY_ADDED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_TOO_MUCH_SATS: { + name: 'ERR_TOO_MUCH_SATS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNAUTHORIZED: { + name: 'ERR_UNAUTHORIZED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNAUTHORIZED_CALLER: { + name: 'ERR_UNAUTHORIZED_CALLER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNAUTHORIZED_SIGNER_REGISTRATION: { + name: 'ERR_UNAUTHORIZED_SIGNER_REGISTRATION', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNSTAKE_IN_PREPARE_PHASE: { + name: 'ERR_UNSTAKE_IN_PREPARE_PHASE', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UPDATE_BOND_SAME_SIGNER: { + name: 'ERR_UPDATE_BOND_SAME_SIGNER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + MAX_NUM_CYCLES: { + name: 'MAX_NUM_CYCLES', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + pOX_5_SIGNER_DOMAIN: { + name: 'POX_5_SIGNER_DOMAIN', + type: { + tuple: [ + { + name: 'chain-id', + type: 'uint128' + }, + { + name: 'name', + type: { + 'string-ascii': { + length: 12 + } + } + }, + { + name: 'version', + type: { + 'string-ascii': { + length: 5 + } + } + } + ] + }, + access: 'constant' +} as TypedAbiVariable<{ + "chainId": bigint; + "name": string; + "version": string; +}>, + PRECISION: { + name: 'PRECISION', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + RESERVE_RATIO: { + name: 'RESERVE_RATIO', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + SIGNER_SET_MIN_USTX: { + name: 'SIGNER_SET_MIN_USTX', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + sIP018_MSG_PREFIX: { + name: 'SIP018_MSG_PREFIX', + type: { + buffer: { + length: 6 + } + }, + access: 'constant' +} as TypedAbiVariable, + STACKS_ADDR_VERSION_MAINNET: { + name: 'STACKS_ADDR_VERSION_MAINNET', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + STACKS_ADDR_VERSION_TESTNET: { + name: 'STACKS_ADDR_VERSION_TESTNET', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + bondAdmin: { + name: 'bond-admin', + type: 'principal', + access: 'variable' +} as TypedAbiVariable, + configured: { + name: 'configured', + type: 'bool', + access: 'variable' +} as TypedAbiVariable, + firstBondPeriodCycle: { + name: 'first-bond-period-cycle', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + firstBurnchainBlockHeight: { + name: 'first-burnchain-block-height', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + firstPox5RewardCycle: { + name: 'first-pox-5-reward-cycle', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + lastAccountedRewardsOnly: { + name: 'last-accounted-rewards-only', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + lastRewardComputeHeight: { + name: 'last-reward-compute-height', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + poxPrepareCycleLength: { + name: 'pox-prepare-cycle-length', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + poxRewardCycleLength: { + name: 'pox-reward-cycle-length', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + reserveBalance: { + name: 'reserve-balance', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + totalSbtcStaked: { + name: 'total-sbtc-staked', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable + }, + constants: { + BOND_GAP_CYCLES: 2n, + BOND_LENGTH_CYCLES: 12n, + ERR_ACTIVE_BOND_NOT_INCLUDED: { + isOk: false, + value: 33n + }, + ERR_ALREADY_REGISTERED: { + isOk: false, + value: 9n + }, + ERR_ALREADY_STAKED: { + isOk: false, + value: 19n + }, + ERR_BOND_ALREADY_SETUP: { + isOk: false, + value: 4n + }, + ERR_BOND_ALREADY_STARTED: { + isOk: false, + value: 43n + }, + ERR_BOND_NOT_ACTIVE: { + isOk: false, + value: 31n + }, + ERR_BOND_NOT_FOUND: { + isOk: false, + value: 7n + }, + eRR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK: { + isOk: false, + value: 35n + }, + ERR_CANNOT_SETUP_BOND_TOO_LATE: { + isOk: false, + value: 3n + }, + ERR_CANNOT_SETUP_BOND_TOO_SOON: { + isOk: false, + value: 2n + }, + ERR_CANNOT_UNSTAKE_SBTC: { + isOk: false, + value: 38n + }, + ERR_DISTRIBUTION_ALREADY_COMPUTED: { + isOk: false, + value: 30n + }, + ERR_INSUFFICIENT_STX: { + isOk: false, + value: 8n + }, + ERR_INVALID_BOND_PERIOD_ORDERING: { + isOk: false, + value: 29n + }, + ERR_INVALID_BTC_HEADER: { + isOk: false, + value: 40n + }, + ERR_INVALID_LOCKUP_AMOUNT: { + isOk: false, + value: 45n + }, + ERR_INVALID_LOCKUP_SCRIPT: { + isOk: false, + value: 42n + }, + ERR_INVALID_MERKLE_PROOF: { + isOk: false, + value: 41n + }, + ERR_INVALID_NUM_CYCLES: { + isOk: false, + value: 20n + }, + ERR_INVALID_OLD_SIGNER_MANAGER: { + isOk: false, + value: 36n + }, + ERR_INVALID_SIGNATURE_PUBKEY: { + isOk: false, + value: 14n + }, + ERR_INVALID_SIGNATURE_RECOVER: { + isOk: false, + value: 13n + }, + ERR_INVALID_START_BURN_HEIGHT: { + isOk: false, + value: 24n + }, + ERR_INVALID_UNSTAKE_SBTC_AMOUNT: { + isOk: false, + value: 37n + }, + ERR_NOT_ALLOWLISTED: { + isOk: false, + value: 11n + }, + ERR_NOT_BOND_PARTICIPANT: { + isOk: false, + value: 34n + }, + ERR_NOT_STAKING: { + isOk: false, + value: 27n + }, + ERR_NO_CLAIMABLE_REWARDS: { + isOk: false, + value: 32n + }, + ERR_READ_TX_OUT_OF_BOUNDS: { + isOk: false, + value: 39n + }, + ERR_SIGNER_KEY_GRANT_NOT_FOUND: { + isOk: false, + value: 17n + }, + ERR_SIGNER_KEY_GRANT_USED: { + isOk: false, + value: 12n + }, + ERR_SIGNER_NOT_FOUND: { + isOk: false, + value: 23n + }, + ERR_STAKER_ALREADY_ADDED: { + isOk: false, + value: 5n + }, + ERR_TOO_MUCH_SATS: { + isOk: false, + value: 10n + }, + ERR_UNAUTHORIZED: { + isOk: false, + value: 1n + }, + ERR_UNAUTHORIZED_CALLER: { + isOk: false, + value: 22n + }, + ERR_UNAUTHORIZED_SIGNER_REGISTRATION: { + isOk: false, + value: 26n + }, + ERR_UNSTAKE_IN_PREPARE_PHASE: { + isOk: false, + value: 28n + }, + ERR_UPDATE_BOND_SAME_SIGNER: { + isOk: false, + value: 44n + }, + MAX_NUM_CYCLES: 96n, + pOX_5_SIGNER_DOMAIN: { + chainId: 2_147_483_648n, + name: 'pox-5-signer', + version: '1.0.0' + }, + PRECISION: 1_000_000_000_000_000_000n, + RESERVE_RATIO: 1_500n, + SIGNER_SET_MIN_USTX: 50_000_000_000n, + sIP018_MSG_PREFIX: Uint8Array.from([83,73,80,48,49,56]), + STACKS_ADDR_VERSION_MAINNET: Uint8Array.from([22]), + STACKS_ADDR_VERSION_TESTNET: Uint8Array.from([26]), + bondAdmin: 'SP000000000000000000002Q6VF78', + configured: false, + firstBondPeriodCycle: 0n, + firstBurnchainBlockHeight: 0n, + firstPox5RewardCycle: 0n, + lastAccountedRewardsOnly: 0n, + lastRewardComputeHeight: 0n, + poxPrepareCycleLength: 50n, + poxRewardCycleLength: 1_050n, + reserveBalance: 0n, + totalSbtcStaked: 0n +}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch40","clarity_version":"Clarity6", + contractName: 'pox-5', + }, +pox5Signer: { + "functions": { + checkpointStakerForIndex: {"name":"checkpoint-staker-for-index","access":"private","args":[{"name":"index-offset","type":"uint128"},{"name":"acc-res","type":{"response":{"ok":{"tuple":[{"name":"first-index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[indexOffset: TypedAbiArg, accRes: TypedAbiArg, "accRes">], Response<{ + "firstIndex": bigint; + "isBond": boolean; + "staker": string; +}, bigint>>, + settleStakerRewards: {"name":"settle-staker-rewards","access":"private","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], { + "earned": bigint; + "rewardsPerToken": bigint; +}>, + updateBondRewardsInfo: {"name":"update-bond-rewards-info","access":"private","args":[{"name":"bond-info","type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}},{"name":"acc","type":"bool"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[bondInfo: TypedAbiArg<{ + "bondIndex": number | bigint; + "earned": number | bigint; + "rewardsPerToken": number | bigint; +}, "bondInfo">, acc: TypedAbiArg], boolean>, + updateRewardsInfo: {"name":"update-rewards-info","access":"private","args":[{"name":"rewards-per-share","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[rewardsPerShare: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], boolean>, + checkpointStaker: {"name":"checkpoint-staker","access":"public","args":[{"name":"staker","type":"principal"},{"name":"first-index","type":"uint128"},{"name":"num-indexes","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstIndex: TypedAbiArg, numIndexes: TypedAbiArg, isBond: TypedAbiArg], Response>, + claimRewards: {"name":"claim-rewards","access":"public","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"bond-totals","type":"uint128"},{"name":"stx-rewards","type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}},{"name":"total-rewards","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg, rewardCycle: TypedAbiArg], Response<{ + "bondRewards": { + "bondIndex": bigint; + "earned": bigint; + "rewardsPerToken": bigint; +}[]; + "bondTotals": bigint; + "stxRewards": { + "earned": bigint; + "rewardsPerToken": bigint; +}; + "totalRewards": bigint; +}, bigint>>, + claimStakerRewards: {"name":"claim-staker-rewards","access":"public","args":[{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[isBond: TypedAbiArg, index: TypedAbiArg], Response>, + registerSelf: {"name":"register-self","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"auth-id","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"signer","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg, authId: TypedAbiArg, signerSig: TypedAbiArg], Response<{ + "signer": string; + "signerKey": Uint8Array; +}, bigint>>, + validateStake_x: {"name":"validate-stake!","access":"public","args":[{"name":"staker","type":"principal"},{"name":"first-index","type":"uint128"},{"name":"num-indexes","type":"uint128"},{"name":"amount-ustx","type":"uint128"},{"name":"amount-sats","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstIndex: TypedAbiArg, numIndexes: TypedAbiArg, amountUstx: TypedAbiArg, amountSats: TypedAbiArg, isBond: TypedAbiArg, signerCalldata: TypedAbiArg], Response>, + getEarnedStakerRewards: {"name":"get-earned-staker-rewards","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getStakerRewardsPerTokenSettledForCycle: {"name":"get-staker-rewards-per-token-settled-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getStakerUnclaimedRewardsForCycle: {"name":"get-staker-unclaimed-rewards-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint> + }, + "maps": { + rewardsPerTokenForCycle: {"name":"rewards-per-token-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; +}, bigint>, + stakerRewardsPerTokenSettledForCycle: {"name":"staker-rewards-per-token-settled-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "staker": string; +}, bigint>, + stakerUnclaimedRewardsForCycle: {"name":"staker-unclaimed-rewards-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "staker": string; +}, bigint> + }, + "variables": { + ERR_NO_CLAIMABLE_REWARDS: { + name: 'ERR_NO_CLAIMABLE_REWARDS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + PRECISION: { + name: 'PRECISION', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable + }, + constants: { + ERR_NO_CLAIMABLE_REWARDS: { + isOk: false, + value: 1_001n + }, + PRECISION: 1_000_000_000_000_000_000n +}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch40","clarity_version":"Clarity6", + contractName: 'pox-5-signer', + }, +sbtcRegistry: { + "functions": { + incrementLastWithdrawalRequestId: {"name":"increment-last-withdrawal-request-id","access":"private","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + completeDeposit: {"name":"complete-deposit","access":"public","args":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"},{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[txid: TypedAbiArg, voutIndex: TypedAbiArg, amount: TypedAbiArg, recipient: TypedAbiArg, burnHash: TypedAbiArg, burnHeight: TypedAbiArg, sweepTxid: TypedAbiArg], Response>, + completeWithdrawalAccept: {"name":"complete-withdrawal-accept","access":"public","args":[{"name":"request-id","type":"uint128"},{"name":"bitcoin-txid","type":{"buffer":{"length":32}}},{"name":"output-index","type":"uint128"},{"name":"signer-bitmap","type":"uint128"},{"name":"fee","type":"uint128"},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[requestId: TypedAbiArg, bitcoinTxid: TypedAbiArg, outputIndex: TypedAbiArg, signerBitmap: TypedAbiArg, fee: TypedAbiArg, burnHash: TypedAbiArg, burnHeight: TypedAbiArg, sweepTxid: TypedAbiArg], Response>, + completeWithdrawalReject: {"name":"complete-withdrawal-reject","access":"public","args":[{"name":"request-id","type":"uint128"},{"name":"signer-bitmap","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[requestId: TypedAbiArg, signerBitmap: TypedAbiArg], Response>, + createWithdrawalRequest: {"name":"create-withdrawal-request","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"max-fee","type":"uint128"},{"name":"sender","type":"principal"},{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"height","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, maxFee: TypedAbiArg, sender: TypedAbiArg, recipient: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "recipient">, height: TypedAbiArg], Response>, + rotateKeys: {"name":"rotate-keys","access":"public","args":[{"name":"new-keys","type":{"list":{"type":{"buffer":{"length":33}},"length":128}}},{"name":"new-address","type":"principal"},{"name":"new-aggregate-pubkey","type":{"buffer":{"length":33}}},{"name":"new-signature-threshold","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newKeys: TypedAbiArg, newAddress: TypedAbiArg, newAggregatePubkey: TypedAbiArg, newSignatureThreshold: TypedAbiArg], Response>, + updateProtocolContract: {"name":"update-protocol-contract","access":"public","args":[{"name":"contract-type","type":{"buffer":{"length":1}}},{"name":"new-contract","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[contractType: TypedAbiArg, newContract: TypedAbiArg], Response>, + getActiveProtocol: {"name":"get-active-protocol","access":"read_only","args":[{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[contractFlag: TypedAbiArg], string | null>, + getCompletedDeposit: {"name":"get-completed-deposit","access":"read_only","args":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"},{"name":"sweep-burn-hash","type":{"buffer":{"length":32}}},{"name":"sweep-burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}]}}}} as TypedAbiFunction<[txid: TypedAbiArg, voutIndex: TypedAbiArg], { + "amount": bigint; + "recipient": string; + "sweepBurnHash": Uint8Array; + "sweepBurnHeight": bigint; + "sweepTxid": Uint8Array; +} | null>, + getCompletedWithdrawalSweepData: {"name":"get-completed-withdrawal-sweep-data","access":"read_only","args":[{"name":"id","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"sweep-burn-hash","type":{"buffer":{"length":32}}},{"name":"sweep-burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}]}}}} as TypedAbiFunction<[id: TypedAbiArg], { + "sweepBurnHash": Uint8Array; + "sweepBurnHeight": bigint; + "sweepTxid": Uint8Array; +} | null>, + getCurrentAggregatePubkey: {"name":"get-current-aggregate-pubkey","access":"read_only","args":[],"outputs":{"type":{"buffer":{"length":33}}}} as TypedAbiFunction<[], Uint8Array>, + getCurrentSignerData: {"name":"get-current-signer-data","access":"read_only","args":[],"outputs":{"type":{"tuple":[{"name":"current-aggregate-pubkey","type":{"buffer":{"length":33}}},{"name":"current-signature-threshold","type":"uint128"},{"name":"current-signer-principal","type":"principal"},{"name":"current-signer-set","type":{"list":{"type":{"buffer":{"length":33}},"length":128}}}]}}} as TypedAbiFunction<[], { + "currentAggregatePubkey": Uint8Array; + "currentSignatureThreshold": bigint; + "currentSignerPrincipal": string; + "currentSignerSet": Uint8Array[]; +}>, + getCurrentSignerPrincipal: {"name":"get-current-signer-principal","access":"read_only","args":[],"outputs":{"type":"principal"}} as TypedAbiFunction<[], string>, + getCurrentSignerSet: {"name":"get-current-signer-set","access":"read_only","args":[],"outputs":{"type":{"list":{"type":{"buffer":{"length":33}},"length":128}}}} as TypedAbiFunction<[], Uint8Array[]>, + getDepositStatus: {"name":"get-deposit-status","access":"read_only","args":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"}],"outputs":{"type":{"optional":"bool"}}} as TypedAbiFunction<[txid: TypedAbiArg, voutIndex: TypedAbiArg], boolean | null>, + getWithdrawalRequest: {"name":"get-withdrawal-request","access":"read_only","args":[{"name":"id","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount","type":"uint128"},{"name":"block-height","type":"uint128"},{"name":"max-fee","type":"uint128"},{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"sender","type":"principal"},{"name":"status","type":{"optional":"bool"}}]}}}} as TypedAbiFunction<[id: TypedAbiArg], { + "amount": bigint; + "blockHeight": bigint; + "maxFee": bigint; + "recipient": { + "hashbytes": Uint8Array; + "version": Uint8Array; +}; + "sender": string; + "status": boolean | null; +} | null>, + isProtocolCaller: {"name":"is-protocol-caller","access":"read_only","args":[{"name":"contract-flag","type":{"buffer":{"length":1}}},{"name":"contract","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[contractFlag: TypedAbiArg, contract: TypedAbiArg], Response> + }, + "maps": { + activeProtocolContracts: {"name":"active-protocol-contracts","key":{"buffer":{"length":1}},"value":"principal"} as TypedAbiMap, + activeProtocolRoles: {"name":"active-protocol-roles","key":"principal","value":{"buffer":{"length":1}}} as TypedAbiMap, + aggregatePubkeys: {"name":"aggregate-pubkeys","key":{"buffer":{"length":33}},"value":"bool"} as TypedAbiMap, + completedDeposits: {"name":"completed-deposits","key":{"tuple":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"}]},"value":{"tuple":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"},{"name":"sweep-burn-hash","type":{"buffer":{"length":32}}},{"name":"sweep-burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}]}} as TypedAbiMap<{ + "txid": Uint8Array; + "voutIndex": number | bigint; +}, { + "amount": bigint; + "recipient": string; + "sweepBurnHash": Uint8Array; + "sweepBurnHeight": bigint; + "sweepTxid": Uint8Array; +}>, + completedWithdrawalSweep: {"name":"completed-withdrawal-sweep","key":"uint128","value":{"tuple":[{"name":"sweep-burn-hash","type":{"buffer":{"length":32}}},{"name":"sweep-burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}]}} as TypedAbiMap, + depositStatus: {"name":"deposit-status","key":{"tuple":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"}]},"value":"bool"} as TypedAbiMap<{ + "txid": Uint8Array; + "voutIndex": number | bigint; +}, boolean>, + withdrawalRequests: {"name":"withdrawal-requests","key":"uint128","value":{"tuple":[{"name":"amount","type":"uint128"},{"name":"block-height","type":"uint128"},{"name":"max-fee","type":"uint128"},{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"sender","type":"principal"}]}} as TypedAbiMap, + withdrawalStatus: {"name":"withdrawal-status","key":"uint128","value":"bool"} as TypedAbiMap + }, + "variables": { + ERR_AGG_PUBKEY_REPLAY: { + name: 'ERR_AGG_PUBKEY_REPLAY', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_REQUEST_ID: { + name: 'ERR_INVALID_REQUEST_ID', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNAUTHORIZED: { + name: 'ERR_UNAUTHORIZED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + depositRole: { + name: 'deposit-role', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + governanceRole: { + name: 'governance-role', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + withdrawalRole: { + name: 'withdrawal-role', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + currentAggregatePubkey: { + name: 'current-aggregate-pubkey', + type: { + buffer: { + length: 33 + } + }, + access: 'variable' +} as TypedAbiVariable, + currentSignatureThreshold: { + name: 'current-signature-threshold', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + currentSignerPrincipal: { + name: 'current-signer-principal', + type: 'principal', + access: 'variable' +} as TypedAbiVariable, + currentSignerSet: { + name: 'current-signer-set', + type: { + list: { + type: { + buffer: { + length: 33 + } + }, + length: 128 + } + }, + access: 'variable' +} as TypedAbiVariable, + lastWithdrawalRequestId: { + name: 'last-withdrawal-request-id', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable + }, + constants: {}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch30","clarity_version":"Clarity3", + contractName: 'sbtc-registry', + }, +sbtcToken: { + "functions": { + protocolMintManyIter: {"name":"protocol-mint-many-iter","access":"private","args":[{"name":"item","type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"}]}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[item: TypedAbiArg<{ + "amount": number | bigint; + "recipient": string; +}, "item">], Response>, + transferManyIter: {"name":"transfer-many-iter","access":"private","args":[{"name":"individual-transfer","type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"memo","type":{"optional":{"buffer":{"length":34}}}},{"name":"sender","type":"principal"},{"name":"to","type":"principal"}]}},{"name":"result","type":{"response":{"ok":"uint128","error":"uint128"}}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[individualTransfer: TypedAbiArg<{ + "amount": number | bigint; + "memo": Uint8Array | null; + "sender": string; + "to": string; +}, "individualTransfer">, result: TypedAbiArg, "result">], Response>, + protocolBurn: {"name":"protocol-burn","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"owner","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, owner: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolBurnLocked: {"name":"protocol-burn-locked","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"owner","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, owner: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolLock: {"name":"protocol-lock","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"owner","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, owner: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolMint: {"name":"protocol-mint","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, recipient: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolMintMany: {"name":"protocol-mint-many","access":"public","args":[{"name":"recipients","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"}]},"length":200}}},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":{"list":{"type":{"response":{"ok":"bool","error":"uint128"}},"length":200}},"error":"uint128"}}}} as TypedAbiFunction<[recipients: TypedAbiArg<{ + "amount": number | bigint; + "recipient": string; +}[], "recipients">, contractFlag: TypedAbiArg], Response[], bigint>>, + protocolSetName: {"name":"protocol-set-name","access":"public","args":[{"name":"new-name","type":{"string-ascii":{"length":32}}},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newName: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolSetSymbol: {"name":"protocol-set-symbol","access":"public","args":[{"name":"new-symbol","type":{"string-ascii":{"length":10}}},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newSymbol: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolSetTokenUri: {"name":"protocol-set-token-uri","access":"public","args":[{"name":"new-uri","type":{"optional":{"string-utf8":{"length":256}}}},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newUri: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolUnlock: {"name":"protocol-unlock","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"owner","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, owner: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + transfer: {"name":"transfer","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"sender","type":"principal"},{"name":"recipient","type":"principal"},{"name":"memo","type":{"optional":{"buffer":{"length":34}}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, sender: TypedAbiArg, recipient: TypedAbiArg, memo: TypedAbiArg], Response>, + transferMany: {"name":"transfer-many","access":"public","args":[{"name":"recipients","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"memo","type":{"optional":{"buffer":{"length":34}}}},{"name":"sender","type":"principal"},{"name":"to","type":"principal"}]},"length":200}}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[recipients: TypedAbiArg<{ + "amount": number | bigint; + "memo": Uint8Array | null; + "sender": string; + "to": string; +}[], "recipients">], Response>, + getBalance: {"name":"get-balance","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, + getBalanceAvailable: {"name":"get-balance-available","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, + getBalanceLocked: {"name":"get-balance-locked","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, + getCurrentAggregatePubkey: {"name":"get-current-aggregate-pubkey","access":"read_only","args":[],"outputs":{"type":{"buffer":{"length":33}}}} as TypedAbiFunction<[], Uint8Array>, + getDecimals: {"name":"get-decimals","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[], Response>, + getName: {"name":"get-name","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"string-ascii":{"length":32}},"error":"none"}}}} as TypedAbiFunction<[], Response>, + getSymbol: {"name":"get-symbol","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"string-ascii":{"length":10}},"error":"none"}}}} as TypedAbiFunction<[], Response>, + getTokenUri: {"name":"get-token-uri","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"optional":{"string-utf8":{"length":256}}},"error":"none"}}}} as TypedAbiFunction<[], Response>, + getTotalSupply: {"name":"get-total-supply","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[], Response> + }, + "maps": { + + }, + "variables": { + ERR_NOT_OWNER: { + name: 'ERR_NOT_OWNER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_TRANSFER_INDEX_PREFIX: { + name: 'ERR_TRANSFER_INDEX_PREFIX', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + tokenDecimals: { + name: 'token-decimals', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + tokenName: { + name: 'token-name', + type: { + 'string-ascii': { + length: 32 + } + }, + access: 'variable' +} as TypedAbiVariable, + tokenSymbol: { + name: 'token-symbol', + type: { + 'string-ascii': { + length: 10 + } + }, + access: 'variable' +} as TypedAbiVariable, + tokenUri: { + name: 'token-uri', + type: { + optional: { + 'string-utf8': { + length: 256 + } + } + }, + access: 'variable' +} as TypedAbiVariable + }, + constants: {}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[{"name":"sbtc-token"},{"name":"sbtc-token-locked"}],"epoch":"Epoch30","clarity_version":"Clarity3", + contractName: 'sbtc-token', + }, +sbtcWithdrawal: { + "functions": { + completeIndividualWithdrawalHelper: {"name":"complete-individual-withdrawal-helper","access":"private","args":[{"name":"withdrawal","type":{"tuple":[{"name":"bitcoin-txid","type":{"optional":{"buffer":{"length":32}}}},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"fee","type":{"optional":"uint128"}},{"name":"output-index","type":{"optional":"uint128"}},{"name":"request-id","type":"uint128"},{"name":"signer-bitmap","type":"uint128"},{"name":"status","type":"bool"},{"name":"sweep-txid","type":{"optional":{"buffer":{"length":32}}}}]}},{"name":"helper-response","type":{"response":{"ok":"uint128","error":"uint128"}}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[withdrawal: TypedAbiArg<{ + "bitcoinTxid": Uint8Array | null; + "burnHash": Uint8Array; + "burnHeight": number | bigint; + "fee": number | bigint | null; + "outputIndex": number | bigint | null; + "requestId": number | bigint; + "signerBitmap": number | bigint; + "status": boolean; + "sweepTxid": Uint8Array | null; +}, "withdrawal">, helperResponse: TypedAbiArg, "helperResponse">], Response>, + acceptWithdrawalRequest: {"name":"accept-withdrawal-request","access":"public","args":[{"name":"request-id","type":"uint128"},{"name":"bitcoin-txid","type":{"buffer":{"length":32}}},{"name":"signer-bitmap","type":"uint128"},{"name":"output-index","type":"uint128"},{"name":"fee","type":"uint128"},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[requestId: TypedAbiArg, bitcoinTxid: TypedAbiArg, signerBitmap: TypedAbiArg, outputIndex: TypedAbiArg, fee: TypedAbiArg, burnHash: TypedAbiArg, burnHeight: TypedAbiArg, sweepTxid: TypedAbiArg], Response>, + completeWithdrawals: {"name":"complete-withdrawals","access":"public","args":[{"name":"withdrawals","type":{"list":{"type":{"tuple":[{"name":"bitcoin-txid","type":{"optional":{"buffer":{"length":32}}}},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"fee","type":{"optional":"uint128"}},{"name":"output-index","type":{"optional":"uint128"}},{"name":"request-id","type":"uint128"},{"name":"signer-bitmap","type":"uint128"},{"name":"status","type":"bool"},{"name":"sweep-txid","type":{"optional":{"buffer":{"length":32}}}}]},"length":600}}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[withdrawals: TypedAbiArg<{ + "bitcoinTxid": Uint8Array | null; + "burnHash": Uint8Array; + "burnHeight": number | bigint; + "fee": number | bigint | null; + "outputIndex": number | bigint | null; + "requestId": number | bigint; + "signerBitmap": number | bigint; + "status": boolean; + "sweepTxid": Uint8Array | null; +}[], "withdrawals">], Response>, + initiateWithdrawalRequest: {"name":"initiate-withdrawal-request","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"max-fee","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, recipient: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "recipient">, maxFee: TypedAbiArg], Response>, + rejectWithdrawalRequest: {"name":"reject-withdrawal-request","access":"public","args":[{"name":"request-id","type":"uint128"},{"name":"signer-bitmap","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[requestId: TypedAbiArg, signerBitmap: TypedAbiArg], Response>, + getBurnHeader: {"name":"get-burn-header","access":"read_only","args":[{"name":"height","type":"uint128"}],"outputs":{"type":{"optional":{"buffer":{"length":32}}}}} as TypedAbiFunction<[height: TypedAbiArg], Uint8Array | null>, + validateRecipient: {"name":"validate-recipient","access":"read_only","args":[{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[recipient: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "recipient">], Response> + }, + "maps": { + + }, + "variables": { + DUST_LIMIT: { + name: 'DUST_LIMIT', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + ERR_ALREADY_PROCESSED: { + name: 'ERR_ALREADY_PROCESSED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_DUST_LIMIT: { + name: 'ERR_DUST_LIMIT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_FEE_TOO_HIGH: { + name: 'ERR_FEE_TOO_HIGH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_ADDR_HASHBYTES: { + name: 'ERR_INVALID_ADDR_HASHBYTES', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_ADDR_VERSION: { + name: 'ERR_INVALID_ADDR_VERSION', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_BURN_HASH: { + name: 'ERR_INVALID_BURN_HASH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_CALLER: { + name: 'ERR_INVALID_CALLER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_REQUEST: { + name: 'ERR_INVALID_REQUEST', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_WITHDRAWAL_INDEX: { + name: 'ERR_WITHDRAWAL_INDEX', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_WITHDRAWAL_INDEX_PREFIX: { + name: 'ERR_WITHDRAWAL_INDEX_PREFIX', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + MAX_ADDRESS_VERSION: { + name: 'MAX_ADDRESS_VERSION', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + mAX_ADDRESS_VERSION_BUFF_20: { + name: 'MAX_ADDRESS_VERSION_BUFF_20', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + mAX_ADDRESS_VERSION_BUFF_32: { + name: 'MAX_ADDRESS_VERSION_BUFF_32', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + withdrawRole: { + name: 'withdraw-role', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable + }, + constants: {}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch30","clarity_version":"Clarity3", + contractName: 'sbtc-withdrawal', + } +} as const; + +export const accounts = {"deployer":{"address":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM","balance":"100000000000000"},"wallet_1":{"address":"ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5","balance":"100000000000000"},"wallet_10":{"address":"ST3FFKYTTB975A3JC3F99MM7TXZJ406R3GKE6JV56","balance":"200000000000000"},"wallet_2":{"address":"ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG","balance":"100000000000000"},"wallet_3":{"address":"ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC","balance":"100000000000000"},"wallet_4":{"address":"ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND","balance":"100000000000000"},"wallet_5":{"address":"ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB","balance":"100000000000000"},"wallet_6":{"address":"ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0","balance":"100000000000000"},"wallet_7":{"address":"ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ","balance":"100000000000000"},"wallet_8":{"address":"ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP","balance":"100000000000000"},"wallet_9":{"address":"STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6","balance":"100000000000000"}} as const; + +export const identifiers = {"pox5":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","pox5Signer":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5-signer","sbtcRegistry":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry","sbtcToken":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token","sbtcWithdrawal":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal"} as const + +export const simnet = { + accounts, + contracts, + identifiers, +} as const; + + +export const deployments = {"pox5":{"devnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","simnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","testnet":null,"mainnet":null},"pox5Signer":{"devnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5-signer","simnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5-signer","testnet":null,"mainnet":null},"sbtcRegistry":{"devnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry","simnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry","testnet":null,"mainnet":null},"sbtcToken":{"devnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token","simnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token","testnet":null,"mainnet":null},"sbtcWithdrawal":{"devnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-withdrawal","simnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal","testnet":null,"mainnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal"}} as const; + +export const project = { + contracts, + deployments, +} as const; + \ No newline at end of file diff --git a/tests/pox5/regtest-env/stacking/common.ts b/tests/pox5/regtest-env/stacking/common.ts new file mode 100644 index 0000000000..952b2e130c --- /dev/null +++ b/tests/pox5/regtest-env/stacking/common.ts @@ -0,0 +1,182 @@ +import { StackingClient } from '@stacks/stacking'; +import { STACKS_TESTNET } from '@stacks/network'; +import { ClarityType, deserializeCV, getAddressFromPrivateKey } from '@stacks/transactions'; +import { getPublicKeyFromPrivate, publicKeyToBtcAddress } from '@stacks/encryption'; +import { createClient } from '@stacks/blockchain-api-client'; +import { Logger, pino } from 'pino'; + +const serviceName = process.env.SERVICE_NAME || 'JS'; +export let logger: Logger; +if (process.env.STACKS_LOG_JSON === '1') { + logger = pino({ + level: process.env.LOG_LEVEL || 'info', + name: serviceName, + }); +} else { + logger = pino({ + name: serviceName, + level: process.env.LOG_LEVEL || 'info', + transport: { + target: 'pino-pretty', + }, + // @ts-ignore + options: { + colorize: true, + }, + }); +} + +export const CHAIN_ID = parseEnvInt('STACKS_CHAIN_ID', false) ?? STACKS_TESTNET.chainId; + +export const nodeUrl = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env.STACKS_CORE_RPC_PORT}`; +export const network = STACKS_TESTNET; +network.chainId = CHAIN_ID; +network.client.baseUrl = nodeUrl; +export const apiClient = createClient({ + baseUrl: nodeUrl, +}); + +export const EPOCH_30_START = parseEnvInt('STACKS_30_HEIGHT', true); +export const EPOCH_25_START = parseEnvInt('STACKS_25_HEIGHT', true); +export const EPOCH_40_START = parseEnvInt('STACKS_40_HEIGHT', true); +export const POX_PREPARE_LENGTH = parseEnvInt('POX_PREPARE_LENGTH', true); +export const POX_REWARD_LENGTH = parseEnvInt('POX_REWARD_LENGTH', true); +export const WALLET_NAME = 'btc_staking'; + +export const accounts = process.env.STACKING_KEYS!.split(',').map((privKey, index) => { + const pubKey = getPublicKeyFromPrivate(privKey); + const stxAddress = getAddressFromPrivateKey(privKey, network); + const signerPrivKey = privKey; + const signerPubKey = getPublicKeyFromPrivate(signerPrivKey); + return { + privKey, + pubKey, + stxAddress, + btcAddr: publicKeyToBtcAddress(pubKey), + signerPrivKey: signerPrivKey, + signerPubKey: signerPubKey, + targetSlots: index + 1, + index, + client: new StackingClient({ + address: stxAddress, + network, + }), + logger: logger.child({ + account: stxAddress, + index: index, + }), + signerManager: `${stxAddress}.signer-manager`, + }; +}); + +export async function fetchAccount(stxAddress: string) { + const url = `${nodeUrl}/v2/accounts/${stxAddress}?proof=0`; + const res = await fetch(url); + const data = (await res.json()) as { + unlock_height: number; + locked: string; + balance: string; + nonce: number; + }; + const locked = deserializeCV(data.locked.slice(2)); + const balance = deserializeCV(data.balance.slice(2)); + if (locked.type !== ClarityType.Int || balance.type !== ClarityType.Int) { + logger.error({ locked, balance }, 'Invalid account data'); + throw new Error('Invalid account data'); + } + return { + unlockHeight: data.unlock_height, + lockedAmount: BigInt(locked.value), + balance: BigInt(balance.value), + nonce: data.nonce, + }; +} + +export type Account = typeof accounts[0]; + +export const MAX_U128 = 2n ** 128n - 1n; +export const maxAmount = MAX_U128; + +export async function waitForSetup() { + try { + await accounts[0]!.client.getPoxInfo(); + } catch (error) { + if ( + error instanceof Error && + 'cause' in error && + error.cause instanceof Error && + /(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause.message) + ) { + console.log(`Stacks node not ready, waiting...`); + } + await new Promise(resolve => setTimeout(resolve, 3000)); + return waitForSetup(); + } +} + +export function parseEnvInt( + envKey: string, + required?: T +): T extends true ? number : number | undefined { + let value = process.env[envKey]; + if (typeof value === 'undefined') { + if (required) { + throw new Error(`Missing required env var: ${envKey}`); + } + return undefined as T extends true ? number : number | undefined; + } + if (value.startsWith('0x')) { + return parseInt(value, 16); + } + return parseInt(value, 10); +} + +export function burnBlockToRewardCycle(burnBlock: number) { + const cycleLength = BigInt(POX_REWARD_LENGTH); + return Number(BigInt(burnBlock) / cycleLength) + 1; +} + +export const EPOCH_30_START_CYCLE = burnBlockToRewardCycle(EPOCH_30_START); + +export function isPreparePhase(burnBlock: number) { + return POX_REWARD_LENGTH - (burnBlock % POX_REWARD_LENGTH) < POX_PREPARE_LENGTH; +} + +export function didCrossPreparePhase(lastBurnHeight: number, newBurnHeight: number) { + return isPreparePhase(newBurnHeight) && !isPreparePhase(lastBurnHeight); +} + +export async function waitForTxConfirmed(txid: string) { + return new Promise((resolve, reject) => { + const startedAt = Date.now(); + const timeoutMs = 120_000; + const interval = setInterval(async () => { + const { data: tx, ...rest } = await apiClient.GET(`/extended/v1/tx/{tx_id}`, { + params: { + path: { + tx_id: txid, + }, + }, + }); + if (!tx) { + if (Date.now() - startedAt > timeoutMs) { + clearInterval(interval); + reject(new Error(`Timed out waiting for tx ${txid}`)); + return; + } + logger.warn({ ...rest }, 'Waiting for tx to be confirmed'); + return; + } + if (tx.tx_status !== 'pending') { + if (tx.tx_status !== 'success') { + logger.error({ ...tx }, 'Tx failed'); + clearInterval(interval); + reject(new Error(`Tx ${txid} failed with status ${tx.tx_status}`)); + return; + } + clearInterval(interval); + resolve(tx); + } + }, 500); + }); +} diff --git a/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar b/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar new file mode 100644 index 0000000000..b3f5672d4a --- /dev/null +++ b/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar @@ -0,0 +1,369 @@ +;; sBTC Registry contract + +;; Error codes +(define-constant ERR_UNAUTHORIZED (err u400)) +(define-constant ERR_INVALID_REQUEST_ID (err u401)) +(define-constant ERR_AGG_PUBKEY_REPLAY (err u402)) + +;; Protocol contract type +(define-constant governance-role 0x00) +(define-constant deposit-role 0x01) +(define-constant withdrawal-role 0x02) + +;; Variables +(define-data-var last-withdrawal-request-id uint u0) +(define-data-var current-signature-threshold uint u0) +(define-data-var current-signer-set (list 128 (buff 33)) (list)) +(define-data-var current-aggregate-pubkey (buff 33) 0x02158613a973bb4469dc9713e0a330a30b6cb88580b772658990a0b052149ca42a) +(define-data-var current-signer-principal principal tx-sender) + +;; Maps +;; Active protocol contracts +(define-map active-protocol-contracts (buff 1) principal) +(map-set active-protocol-contracts governance-role .sbtc-bootstrap-signers) +(map-set active-protocol-contracts deposit-role .sbtc-deposit) +(map-set active-protocol-contracts withdrawal-role .sbtc-withdrawal) +;; Role for active protocol contracts +(define-map active-protocol-roles principal (buff 1)) +(map-set active-protocol-roles .sbtc-bootstrap-signers governance-role) +(map-set active-protocol-roles .sbtc-deposit deposit-role) +(map-set active-protocol-roles .sbtc-withdrawal withdrawal-role) +;; Internal data structure to store withdrawal +;; requests. Requests are associated with a unique +;; request ID. +(define-map withdrawal-requests uint { + ;; Amount of sBTC being withdrawaled (in sats) + amount: uint, + max-fee: uint, + sender: principal, + ;; BTC recipient address in the same format of + ;; pox contracts + recipient: { + version: (buff 1), + hashbytes: (buff 32), + }, + ;; Burn block height where the withdrawal request was + ;; created + block-height: uint, +}) + +;; Data structure to map request-id to status +;; If status is `none`, the request is pending. +;; Otherwise, the boolean value indicates whether +;; the withdrawal was accepted. +(define-map withdrawal-status uint bool) + +;; Data structure to map successful withdrawal requests +;; to their respective sweep transaction. Stores the +;; txid, burn hash, and burn height. +(define-map completed-withdrawal-sweep uint { + sweep-txid: (buff 32), + sweep-burn-hash: (buff 32), + sweep-burn-height: uint, +}) + +;; Internal data structure to store completed +;; deposit requests & avoid replay attacks. +(define-map deposit-status {txid: (buff 32), vout-index: uint} bool) + +;; Data structure to map successful deposit requests +;; to their respective sweep transaction. Stores the +;; txid, burn hash, and burn height. +(define-map completed-deposits {txid: (buff 32), vout-index: uint} + { + amount: uint, + recipient: principal, + sweep-txid: (buff 32), + sweep-burn-hash: (buff 32), + sweep-burn-height: uint, + } +) + +;; Data structure to store aggregate pubkey, +;; stored to avoid replay +(define-map aggregate-pubkeys (buff 33) bool) + +;; Read-only functions +;; Get a withdrawal request by its ID. +;; This function returns the fields of the withdrawal +;; request, along with its status. +(define-read-only (get-withdrawal-request (id uint)) + (match (map-get? withdrawal-requests id) + request (some (merge request { + status: (map-get? withdrawal-status id) + })) + none + ) +) + +;; Get a completed withdrawal sweep data by its request ID. +;; This function returns the fields of the withdrawal-sweeps map. +(define-read-only (get-completed-withdrawal-sweep-data (id uint)) + (map-get? completed-withdrawal-sweep id) +) + +;; Get a completed deposit by its transaction ID & vout index. +;; This function returns the fields of the completed-deposits map. +(define-read-only (get-completed-deposit (txid (buff 32)) (vout-index uint)) + (map-get? completed-deposits {txid: txid, vout-index: vout-index}) +) + +;; Get a completed deposit sweep data by its transaction ID & vout index. +;; This function returns the fields of the completed-deposits map. +(define-read-only (get-deposit-status (txid (buff 32)) (vout-index uint)) + (map-get? deposit-status {txid: txid, vout-index: vout-index}) +) + +;; Get the current signer set. +;; This function returns the current signer set as a list of principals. +(define-read-only (get-current-signer-data) + { + current-signer-set: (var-get current-signer-set), + current-aggregate-pubkey: (var-get current-aggregate-pubkey), + current-signer-principal: (var-get current-signer-principal), + current-signature-threshold: (var-get current-signature-threshold), + } +) + +;; Get the current aggregate pubkey. +;; This function returns the current aggregate pubkey. +(define-read-only (get-current-aggregate-pubkey) + (var-get current-aggregate-pubkey) +) + +;; Get the current signer principal. +;; This function returns the current signer principal. +(define-read-only (get-current-signer-principal) + (var-get current-signer-principal) +) + +(define-read-only (get-current-signer-set) + (var-get current-signer-set) +) + +(define-read-only (get-active-protocol (contract-flag (buff 1))) + (map-get? active-protocol-contracts contract-flag) +) + + +;; Public functions + +;; Store a new withdrawal request. +;; Note that this function can only be called by other sBTC +;; contracts - it cannot be called by users directly. +;; +;; This function does not handle validation or moving the funds. +;; Instead, it is purely for the purpose of storing the request. +;; +;; The function will emit a print event with the topic "withdrawal-create" +;; and the data of the request. +(define-public (create-withdrawal-request + (amount uint) + (max-fee uint) + (sender principal) + (recipient { version: (buff 1), hashbytes: (buff 32) }) + (height uint) + ) + (let + ( + (id (increment-last-withdrawal-request-id)) + ) + (try! (is-protocol-caller withdrawal-role contract-caller)) + ;; #[allow(unchecked_data)] + (map-insert withdrawal-requests id { + amount: amount, + max-fee: max-fee, + sender: sender, + recipient: recipient, + block-height: height, + }) + (print { + topic: "withdrawal-create", + amount: amount, + request-id: id, + sender: sender, + recipient: recipient, + block-height: height, + max-fee: max-fee, + }) + (ok id) + ) +) + +;; Complete withdrawal request by noting the acceptance in the +;; withdrawal-status state map. +;; +;; This function will emit a print event with the topic +;; "withdrawal-accept". +(define-public (complete-withdrawal-accept + (request-id uint) + (bitcoin-txid (buff 32)) + (output-index uint) + (signer-bitmap uint) + (fee uint) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32)) + ) + (begin + (try! (is-protocol-caller withdrawal-role contract-caller)) + ;; Mark the withdrawal as completed + (map-insert withdrawal-status request-id true) + (map-insert completed-withdrawal-sweep request-id { + sweep-txid: sweep-txid, + sweep-burn-hash: burn-hash, + sweep-burn-height: burn-height, + }) + (print { + topic: "withdrawal-accept", + request-id: request-id, + bitcoin-txid: bitcoin-txid, + signer-bitmap: signer-bitmap, + output-index: output-index, + fee: fee, + burn-hash: burn-hash, + burn-height: burn-height, + sweep-txid: sweep-txid, + }) + (ok true) + ) +) + +;; Complete withdrawal request by noting the rejection in the +;; withdrawal-status state map. +;; +;; This function will emit a print event with the topic +;; "withdrawal-reject". +(define-public (complete-withdrawal-reject + (request-id uint) + (signer-bitmap uint) + ) + (begin + (try! (is-protocol-caller withdrawal-role contract-caller)) + ;; Mark the withdrawal as completed + (map-insert withdrawal-status request-id false) + (print { + topic: "withdrawal-reject", + request-id: request-id, + signer-bitmap: signer-bitmap, + }) + (ok true) + ) +) + +;; Store a new insert request. +;; Note that this function can only be called by other sBTC +;; contracts (specifically the current version of the deposit contract) +;; - it cannot be called by users directly. +;; +;; This function does not handle validation or moving the funds. +;; Instead, it is purely for the purpose of storing the completed deposit. +(define-public (complete-deposit + (txid (buff 32)) + (vout-index uint) + (amount uint) + (recipient principal) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32)) + ) + (begin + (try! (is-protocol-caller deposit-role contract-caller)) + (map-insert deposit-status {txid: txid, vout-index: vout-index} true) + (map-insert completed-deposits {txid: txid, vout-index: vout-index} { + amount: amount, + recipient: recipient, + sweep-txid: sweep-txid, + sweep-burn-hash: burn-hash, + sweep-burn-height: burn-height, + }) + (print { + topic: "completed-deposit", + bitcoin-txid: txid, + output-index: vout-index, + amount: amount, + burn-hash: burn-hash, + burn-height: burn-height, + sweep-txid: sweep-txid, + }) + (ok true) + ) +) + +;; Rotate the signer set, multi-sig principal, & aggregate pubkey +;; This function can only be called by the bootstrap-signers contract. +(define-public (rotate-keys + (new-keys (list 128 (buff 33))) + (new-address principal) + (new-aggregate-pubkey (buff 33)) + (new-signature-threshold uint) + ) + (begin + ;; Check that caller is protocol contract + (try! (is-protocol-caller governance-role contract-caller)) + ;; Check that the aggregate pubkey is not already in the map + (asserts! (map-insert aggregate-pubkeys new-aggregate-pubkey true) ERR_AGG_PUBKEY_REPLAY) + ;; Update the current signer set + (var-set current-signer-set new-keys) + ;; Update the current multi-sig address + (var-set current-signer-principal new-address) + ;; Update the current signature threshold + (var-set current-signature-threshold new-signature-threshold) + ;; Update the current aggregate pubkey + (var-set current-aggregate-pubkey new-aggregate-pubkey) + (print { + topic: "key-rotation", + new-keys: new-keys, + new-address: new-address, + new-aggregate-pubkey: new-aggregate-pubkey, + new-signature-threshold: new-signature-threshold + }) + (ok true) + ) +) + +;; Update protocol contract +;; This function can only be called by the active bootstrap-signers contract +(define-public (update-protocol-contract + (contract-type (buff 1)) + (new-contract principal) + ) + (begin + ;; Check that caller is protocol contract + (try! (is-protocol-caller governance-role contract-caller)) + ;; Update the protocol contract + (map-set active-protocol-contracts contract-type new-contract) + ;; Update the protocol role + (map-set active-protocol-roles new-contract contract-type) + (print { + topic: "update-protocol-contract", + contract-type: contract-type, + new-contract: new-contract, + }) + (ok true) + ) +) + +;; Private functions +;; Increment the last withdrawal request ID and +;; return the new value. +(define-private (increment-last-withdrawal-request-id) + (let + ( + (next-value (+ u1 (var-get last-withdrawal-request-id))) + ) + (var-set last-withdrawal-request-id next-value) + next-value + ) +) + +;; Checks whether the contract-caller is a protocol contract +(define-read-only (is-protocol-caller (contract-flag (buff 1)) (contract principal)) + (begin + ;; Check that contract-caller is an protocol contract + (asserts! (is-eq (some contract) (map-get? active-protocol-contracts contract-flag)) ERR_UNAUTHORIZED) + ;; Check that flag matches the contract-caller + (asserts! (is-eq (some contract-flag) (map-get? active-protocol-roles contract)) ERR_UNAUTHORIZED) + (ok true) + ) +) diff --git a/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar b/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar new file mode 100644 index 0000000000..dcbb9477cb --- /dev/null +++ b/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar @@ -0,0 +1,160 @@ +(define-constant ERR_NOT_OWNER (err u4)) ;; `tx-sender` or `contract-caller` tried to move a token it does not own. +(define-constant ERR_TRANSFER_INDEX_PREFIX u1000) + +(define-fungible-token sbtc-token) +(define-fungible-token sbtc-token-locked) + +(define-data-var token-name (string-ascii 32) "sBTC") +(define-data-var token-symbol (string-ascii 10) "sBTC") +(define-data-var token-uri (optional (string-utf8 256)) (some u"https://ipfs.io/ipfs/bafkreibqnozdui4ntgoh3oo437lvhg7qrsccmbzhgumwwjf2smb3eegyqu")) +(define-constant token-decimals u8) + +(define-read-only (get-current-aggregate-pubkey) 0x0204cff1ade0cc7f74d1b5a2b7c7bee653cfb5e6c0dce360795d314c829c4aaf52) + +;; --- Protocol functions + +(define-public (protocol-lock (amount uint) (owner principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (try! (ft-burn? sbtc-token amount owner)) + (ft-mint? sbtc-token-locked amount owner) + ) +) + +(define-public (protocol-unlock (amount uint) (owner principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (try! (ft-burn? sbtc-token-locked amount owner)) + (ft-mint? sbtc-token amount owner) + ) +) + +(define-public (protocol-mint (amount uint) (recipient principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ft-mint? sbtc-token amount recipient) + ) +) + +(define-public (protocol-burn (amount uint) (owner principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ft-burn? sbtc-token amount owner) + ) +) + +(define-public (protocol-burn-locked (amount uint) (owner principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ft-burn? sbtc-token-locked amount owner) + ) +) + +(define-public (protocol-set-name (new-name (string-ascii 32)) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ok (var-set token-name new-name)) + ) +) + +(define-public (protocol-set-symbol (new-symbol (string-ascii 10)) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ok (var-set token-symbol new-symbol)) + ) +) + +(define-public (protocol-set-token-uri (new-uri (optional (string-utf8 256))) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ok (var-set token-uri new-uri)) + ) +) + +(define-private (protocol-mint-many-iter (item {amount: uint, recipient: principal})) + (ft-mint? sbtc-token (get amount item) (get recipient item)) +) + +(define-public (protocol-mint-many (recipients (list 200 {amount: uint, recipient: principal})) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ok (map protocol-mint-many-iter recipients)) + ) +) + +;; --- Public functions +(define-public (transfer-many + (recipients (list 200 { + amount: uint, + sender: principal, + to: principal, + memo: (optional (buff 34)) }))) + (fold transfer-many-iter recipients (ok u0)) +) + +(define-private (transfer-many-iter + (individual-transfer { + amount: uint, + sender: principal, + to: principal, + memo: (optional (buff 34)) }) + (result (response uint uint))) + (match result + index + (begin + (unwrap! + (transfer + (get amount individual-transfer) + (get sender individual-transfer) + (get to individual-transfer) + (get memo individual-transfer)) + (err (+ ERR_TRANSFER_INDEX_PREFIX index))) + (ok (+ index u1)) + ) + err-index + (err err-index) + ) +) + +;; sip-010-trait + +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) ERR_NOT_OWNER) + (try! (ft-transfer? sbtc-token amount sender recipient)) + (match memo to-print (print to-print) 0x) + (ok true) + ) +) + +(define-read-only (get-name) + (ok (var-get token-name)) +) + +(define-read-only (get-symbol) + (ok (var-get token-symbol)) +) + +(define-read-only (get-decimals) + (ok token-decimals) +) + +(define-read-only (get-balance (who principal)) + (ok (+ (ft-get-balance sbtc-token who) (ft-get-balance sbtc-token-locked who))) +) + +(define-read-only (get-balance-available (who principal)) + (ok (ft-get-balance sbtc-token who)) +) + +(define-read-only (get-balance-locked (who principal)) + (ok (ft-get-balance sbtc-token-locked who)) +) + +(define-read-only (get-total-supply) + (ok (+ (ft-get-supply sbtc-token) (ft-get-supply sbtc-token-locked))) +) + +(define-read-only (get-token-uri) + (ok (var-get token-uri)) +) \ No newline at end of file diff --git a/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar b/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar new file mode 100644 index 0000000000..22aaa77212 --- /dev/null +++ b/tests/pox5/regtest-env/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar @@ -0,0 +1,310 @@ +;; Error codes + +;; The `version` part of the recipient address is invalid +(define-constant ERR_INVALID_ADDR_VERSION (err u500)) +;; The `hashbytes` part of the recipient address is invalid +(define-constant ERR_INVALID_ADDR_HASHBYTES (err u501)) +;; The size of the withdrawal is smaller than the dust limit +(define-constant ERR_DUST_LIMIT (err u502)) +;; The request id was invalid / returned 'none' +(define-constant ERR_INVALID_REQUEST (err u503)) +;; The caller is not the currently-governing multisig principal +(define-constant ERR_INVALID_CALLER (err u504)) +;; The withdrawal request was already processed +(define-constant ERR_ALREADY_PROCESSED (err u505)) +;; The paid fee was higher than requested +(define-constant ERR_FEE_TOO_HIGH (err u506)) +;; The returned index marks the failed transaction in list +(define-constant ERR_WITHDRAWAL_INDEX_PREFIX (unwrap-err! ERR_WITHDRAWAL_INDEX (err true))) +(define-constant ERR_WITHDRAWAL_INDEX (err u507)) +(define-constant ERR_INVALID_BURN_HASH (err u508)) + +;; Maximum value of an address version as a uint +(define-constant MAX_ADDRESS_VERSION u6) +;; Maximum value of an address version that has a 20-byte hashbytes +;; (0x00, 0x01, 0x02, 0x03, and 0x04 have 20-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_20 u4) +;; Maximum value of an address version that has a 32-byte hashbytes +;; (0x05 and 0x06 have 32-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_32 u6) +;; The minimum amount of sBTC you can withdraw +(define-constant DUST_LIMIT u546) +;; protocol contract type +(define-constant withdraw-role 0x02) + +;; Initiate a new withdrawal request. +;; +;; # Notes +;; +;; ## Amounts +;; +;; This function locks up `amount + max-fee` from the tx-sender's account, +;; and when the withdrawal request is accepted, the signers will send +;; `amount` of sats to the recipient and spend an a fee amount to bitcoin +;; miners where fee less than or equal to max-fee. If fee is less than +;; max-fee, then the difference will be minted back to the user when +;; `accept-withdrawal-request` is invoked. +;; +;; ## The recipient +;; +;; This constraints and meaning of the recipient field is summarized as: +;; ```text +;; version == 0x00 and (len hashbytes) == 20 => P2PKH +;; version == 0x01 and (len hashbytes) == 20 => P2SH +;; version == 0x02 and (len hashbytes) == 20 => P2SH-P2WPKH +;; version == 0x03 and (len hashbytes) == 20 => P2SH-P2WSH +;; version == 0x04 and (len hashbytes) == 20 => P2WPKH +;; version == 0x05 and (len hashbytes) == 32 => P2WSH +;; version == 0x06 and (len hashbytes) == 32 => P2TR +;; ``` +;; Also see +;; +;; Below is a detailed breakdown of bitcoin address types and how they map +;; to the clarity value. In what follows below, the network used for the +;; human-readable parts is inherited from the network of the underlying +;; transaction itself (basically, on stacks mainnet we send to mainnet +;; bitcoin addresses and similarly on stacks testnet we send to bitcoin +;; testnet addresses). +;; +;; ### P2PKH +;; +;; Generally speaking, Pay-to-Public-Key-Hash addresses are formed by +;; taking the Hash160 of the public key, prefixing it with one byte (0x00 +;; on mainnet and 0x6F on testing) and then base58 encoding the result. +;; +;; To specify this address type as the recipient, the `version` is 0x00 and +;; the `hashbytes` is the Hash160 of the public key. +;; +;; +;; ### P2SH, P2SH-P2WPKH, and P2SH-P2WSH +;; +;; Pay-to-script-hash-* addresses are formed by taking the Hash160 of the +;; locking script, prefixing it with one byte (0x05 on mainnet and 0xC4 on +;; testnet) and base58 encoding the result. The difference between them +;; lies with the locking script. For P2SH-P2WPKH addresses, the locking +;; script is: +;; ```text +;; 0 || +;; ``` +;; For P2SH-P2WSH addresses, the locking script is: +;; ```text +;; 0 || +;; ``` +;; And for P2SH addresses you get to choose the locking script in its +;; entirety. +;; +;; Again, after you construct the locking script you take its Hash160, +;; prefix it with one byte and base58 encode it to form the address. To +;; specify these address types in the recipient, the `version` is 0x01, +;; 0x02, and 0x03 (for P2SH, P2SH-P2WPKH, and P2SH-P2WSH respectively) with +;; the `hashbytes` is the Hash160 of the locking script. +;; +;; +;; ### P2WPKH +;; +;; Pay-to-witness-public-key-hash addresses are formed by creating a +;; witness program made entirely of the Hash160 of the compressed public +;; key. +;; +;; To specify this address type in the recipient, the `version` is 0x04 and +;; the `hashbytes` is the Hash160 of the compressed public key. +;; +;; +;; ### P2WSH +;; +;; Pay-to-witness-script-hash addresses are formed by taking a witness +;; program that is compressed entirely of the SHA256 of the redeem script. +;; +;; To specify this address type in the recipient, the `version` is 0x05 and +;; the `hashbytes` is the SHA256 of the redeem script. +;; +;; +;; ### P2TR +;; +;; Pay-to-taproot addresses are formed by "tweaking" the x-coordinate of a +;; public key with a merkle tree. The result of the tweak is used as the +;; witness program for the address. +;; +;; To specify this address type in the recipient, the `version` is 0x06 and +;; the `hashbytes` is the "tweaked" public key. +(define-public (initiate-withdrawal-request (amount uint) + (recipient { version: (buff 1), hashbytes: (buff 32) }) + (max-fee uint) + ) + (begin + (try! (contract-call? .sbtc-token protocol-lock (+ amount max-fee) tx-sender withdraw-role)) + (asserts! (> amount DUST_LIMIT) ERR_DUST_LIMIT) + + ;; Validate the recipient address + (try! (validate-recipient recipient)) + + (ok (try! (contract-call? .sbtc-registry create-withdrawal-request amount max-fee tx-sender recipient burn-block-height))) + ) +) + +;; Accept a withdrawal request +(define-public (accept-withdrawal-request (request-id uint) + (bitcoin-txid (buff 32)) + (signer-bitmap uint) + (output-index uint) + (fee uint) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32))) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + (request (unwrap! (contract-call? .sbtc-registry get-withdrawal-request request-id) ERR_INVALID_REQUEST)) + (requested-max-fee (get max-fee request)) + (requested-amount (get amount request)) + (requester (get sender request)) + ) + + ;; Verify that Bitcoin hasn't forked by comparing the burn hash provided + (asserts! (is-eq (some burn-hash) (get-burn-header burn-height)) ERR_INVALID_BURN_HASH) + + ;; Check that the caller is the current signer principal + (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) + + ;; Check whether it was already accepted or rejected + (asserts! (is-none (get status request)) ERR_ALREADY_PROCESSED) + + ;; Check that fee is not higher than requesters max fee + (asserts! (<= fee requested-max-fee) ERR_FEE_TOO_HIGH) + + ;; Burn the locked-sbtc + (try! (contract-call? .sbtc-token protocol-burn-locked (+ requested-amount requested-max-fee) requester withdraw-role)) + + ;; Mint the difference b/w max-fee of the request & fee actually paid back to the user in sBTC + (if (is-eq (- requested-max-fee fee) u0) + true + (try! (contract-call? .sbtc-token protocol-mint (- requested-max-fee fee) requester withdraw-role)) + ) + + ;; Call into registry to confirm accepted withdrawal + (try! (contract-call? .sbtc-registry complete-withdrawal-accept request-id bitcoin-txid output-index signer-bitmap fee burn-hash burn-height sweep-txid)) + + (ok true) + ) +) + +;; Reject a withdrawal request +(define-public (reject-withdrawal-request (request-id uint) (signer-bitmap uint)) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + (withdrawal (unwrap! (contract-call? .sbtc-registry get-withdrawal-request request-id) ERR_INVALID_REQUEST)) + (requested-max-fee (get max-fee withdrawal)) + (requested-amount (get amount withdrawal)) + (requester (get sender withdrawal)) + ) + + ;; Check that the caller is the current signer principal + (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) + + ;; Check that request status is currently-pending + (asserts! (is-none (get status withdrawal)) ERR_ALREADY_PROCESSED) + + ;; Burn sbtc-locked & re-mint sbtc to original requester + (try! (contract-call? .sbtc-token protocol-unlock (+ requested-amount requested-max-fee) requester withdraw-role)) + + ;; Call into registry to confirm accepted withdrawal + (try! (contract-call? .sbtc-registry complete-withdrawal-reject request-id signer-bitmap)) + + (ok true) + ) +) +;; Complete multiple withdrawal requests +(define-public (complete-withdrawals (withdrawals (list 600 + {request-id: uint, + status: bool, + signer-bitmap: uint, + bitcoin-txid: (optional (buff 32)), + output-index: (optional uint), + fee: (optional uint), + burn-hash: (buff 32), + burn-height: uint, + sweep-txid: (optional (buff 32))}))) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + ) + + ;; Check that the caller is the current signer principal + (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) + + (fold complete-individual-withdrawal-helper withdrawals (ok u0)) + ) +) + +(define-private (complete-individual-withdrawal-helper (withdrawal + {request-id: uint, + status: bool, + signer-bitmap: uint, + bitcoin-txid: (optional (buff 32)), + output-index: (optional uint), + fee: (optional uint), + burn-hash: (buff 32), + burn-height: uint, + sweep-txid: (optional (buff 32))}) + (helper-response (response uint uint))) + (match helper-response + index + (let + ( + (current-request-id (get request-id withdrawal)) + (current-signer-bitmap (get signer-bitmap withdrawal)) + (current-bitcoin-txid (get bitcoin-txid withdrawal)) + (current-output-index (get output-index withdrawal)) + (current-fee (get fee withdrawal)) + ) + (if (get status withdrawal) + ;; accepted + (begin + (asserts! + (and (is-some current-bitcoin-txid) (is-some current-output-index) (is-some current-fee)) + (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) + (unwrap! (accept-withdrawal-request current-request-id (unwrap-panic current-bitcoin-txid) current-signer-bitmap (unwrap-panic current-output-index) (unwrap-panic current-fee) (get burn-hash withdrawal) (get burn-height withdrawal) (unwrap-panic (get sweep-txid withdrawal))) (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) + ) + ;; rejected + (unwrap! (reject-withdrawal-request current-request-id current-signer-bitmap) (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) + ) + (ok (+ index u1)) + ) + err-response + (err err-response) + ) +) + +;; Validation methods + +;; Validate that a withdrawal's recipient address is well-formed. The logic +;; here follows the same rules as pox-4. +;; +;; At a high-level, the version must be a uint between 0 and 6 (inclusive), +;; and the length of the hashbytes must be 20 bytes if the version is <= 4, +;; and 32 bytes if the version is 5 or 6. +(define-read-only (validate-recipient (recipient { version: (buff 1), hashbytes: (buff 32) })) + (let + ( + (version (get version recipient)) + (hashbytes (get hashbytes recipient)) + (version-int (buff-to-uint-be version)) + ) + ;; Validate the `version` + (asserts! (<= version-int MAX_ADDRESS_VERSION) ERR_INVALID_ADDR_VERSION) + ;; Validate the length of `hashbytes` + (asserts! (if (<= version-int MAX_ADDRESS_VERSION_BUFF_20) + ;; If version is <= 4, then hashbytes must be 20 bytes + (is-eq (len hashbytes) u20) + ;; Otherwise, hashbytes must be 32 bytes + (is-eq (len hashbytes) u32)) + ERR_INVALID_ADDR_HASHBYTES) + (ok true) + ) +) + +;; Return the bitcoin header hash of the bitcoin block at the given height. +(define-read-only (get-burn-header (height uint)) + (get-burn-block-info? header-hash height) +) diff --git a/tests/pox5/regtest-env/stacking/contracts/pox-5-signer.clar b/tests/pox5/regtest-env/stacking/contracts/pox-5-signer.clar new file mode 100644 index 0000000000..4e11a18c7e --- /dev/null +++ b/tests/pox5/regtest-env/stacking/contracts/pox-5-signer.clar @@ -0,0 +1,289 @@ +(impl-trait .pox-5.signer-manager-trait) +(use-trait signer-manager-trait .pox-5.signer-manager-trait) + +(define-constant ERR_NO_CLAIMABLE_REWARDS (err u1001)) + +;; Used to prevent fractional multiplication errors +;; during reward calculations +(define-constant PRECISION u1000000000000000000) ;; 1e18 + +(define-map rewards-per-token-for-cycle + { + index: uint, + is-bond: bool, + } + uint +) + +(define-map staker-rewards-per-token-settled-for-cycle + { + is-bond: bool, + index: uint, + staker: principal, + } + uint +) + +;; Represents pending, but unclaimed rewards for a staker +(define-map staker-unclaimed-rewards-for-cycle + { + is-bond: bool, + index: uint, + staker: principal, + } + uint +) + +;; #[allow(unnecessary_public)] +(define-public (validate-stake! + ;; #[allow(unused_binding)] + (staker principal) + ;; #[allow(unused_binding)] + (first-index uint) + ;; #[allow(unused_binding)] + (num-indexes uint) + ;; #[allow(unused_binding)] + (amount-ustx uint) + ;; #[allow(unused_binding)] + (amount-sats uint) + ;; #[allow(unused_binding)] + (is-bond bool) + ;; #[allow(unused_binding)] + (signer-calldata (optional (buff 500))) + ) + (ok true) +) + +(define-public (register-self + (signer-manager ) + (signer-key (buff 33)) + (auth-id uint) + (signer-sig (buff 65)) + ) + (as-contract? () + (try! (contract-call? .pox-5 grant-signer-key signer-key current-contract + auth-id signer-sig + )) + (try! (contract-call? .pox-5 register-signer signer-manager signer-key)) + ) +) + +;; Handling rewards checkpointing for a staker +(define-public (checkpoint-staker + (staker principal) + (first-index uint) + (num-indexes uint) + (is-bond bool) + ) + (begin + (try! (fold checkpoint-staker-for-index + (unwrap-panic (slice? + (list + u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 + u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 + u30 u31 u32 u33 u34 u35 u36 u37 u38 u39 u40 u41 u42 u43 + u44 u45 u46 u47 u48 u49 u50 u51 u52 u53 u54 u55 u56 u57 + u58 u59 u60 u61 u62 u63 u64 u65 u66 u67 u68 u69 u70 u71 + u72 u73 u74 u75 u76 u77 u78 u79 u80 u81 u82 u83 u84 u85 + u86 u87 u88 u89 u90 u91 u92 u93 u94 u95 + ) + u0 num-indexes + )) + (ok { + staker: staker, + first-index: first-index, + is-bond: is-bond, + }) + )) + (ok true) + ) +) + +(define-private (checkpoint-staker-for-index + (index-offset uint) + (acc-res (response { + staker: principal, + first-index: uint, + is-bond: bool, + } + uint + )) + ) + (let ( + (acc (try! acc-res)) + (staker (get staker acc)) + (index (+ (get first-index acc) index-offset)) + ) + (settle-staker-rewards staker (get is-bond acc) index) + (ok acc) + ) +) + +(define-private (settle-staker-rewards + (staker principal) + (is-bond bool) + (index uint) + ) + (let ( + (earned (get-earned-staker-rewards staker is-bond index)) + (rewards-per-token (get-rewards-per-token-for-cycle is-bond index)) + ) + (map-set staker-unclaimed-rewards-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + } + earned + ) + (map-set staker-rewards-per-token-settled-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + } + rewards-per-token + ) + { + earned: earned, + rewards-per-token: rewards-per-token, + } + ) +) + +(define-public (claim-rewards + (bond-periods (list 6 uint)) + (reward-cycle uint) + ) + (let ((new-rewards-info (try! (as-contract? () + (try! (contract-call? .pox-5 claim-rewards bond-periods reward-cycle)) + )))) + (update-rewards-info + (get rewards-per-token (get stx-rewards new-rewards-info)) false + reward-cycle + ) + (fold update-bond-rewards-info (get bond-rewards new-rewards-info) true) + (ok new-rewards-info) + ) +) + +;; Get the total amount of rewards earned since the last +;; rewards snapshot for this staker. +;; +;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` +(define-read-only (get-earned-staker-rewards + (staker principal) + (is-bond bool) + (index uint) + ) + (let ( + (shares (contract-call? .pox-5 get-staker-shares-staked-for-cycle staker + is-bond index current-contract + )) + (rpt-current (get-rewards-per-token-for-cycle is-bond index)) + (rpt-paid (get-staker-rewards-per-token-settled-for-cycle staker is-bond index)) + (pending (get-staker-unclaimed-rewards-for-cycle staker is-bond index)) + (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) + ) + (+ pending newly-earned) + ) +) + +(define-public (claim-staker-rewards + (is-bond bool) + (index uint) + ) + (let ( + (staker tx-sender) + (rewards-info (settle-staker-rewards staker is-bond index)) + (earned (get earned rewards-info)) + ) + (asserts! (> earned u0) ERR_NO_CLAIMABLE_REWARDS) + (map-set staker-unclaimed-rewards-for-cycle { + staker: staker, + is-bond: is-bond, + index: index, + } + u0 + ) + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" earned + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer earned tx-sender staker none + )) + )) + (ok earned) + ) +) + +(define-private (update-rewards-info + (rewards-per-share uint) + (is-bond bool) + (index uint) + ) + (begin + (map-set rewards-per-token-for-cycle { + index: index, + is-bond: is-bond, + } + rewards-per-share + ) + ) +) + +(define-private (update-bond-rewards-info + (bond-info { + bond-index: uint, + earned: uint, + rewards-per-token: uint, + }) + ;; #[allow(unused_binding)] + (acc bool) + ) + (map-set rewards-per-token-for-cycle { + is-bond: true, + index: (get bond-index bond-info), + } + (get rewards-per-token bond-info) + ) +) + +(define-read-only (get-rewards-per-token-for-cycle + (is-bond bool) + (index uint) + ) + (default-to u0 + (map-get? rewards-per-token-for-cycle { + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-staker-rewards-per-token-settled-for-cycle + (staker principal) + (is-bond bool) + (index uint) + ) + (default-to u0 + (map-get? staker-rewards-per-token-settled-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-staker-unclaimed-rewards-for-cycle + (staker principal) + (is-bond bool) + (index uint) + ) + (default-to u0 + (map-get? staker-unclaimed-rewards-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + }) + ) +) diff --git a/tests/pox5/regtest-env/stacking/package.json b/tests/pox5/regtest-env/stacking/package.json new file mode 100644 index 0000000000..d9bf9e645b --- /dev/null +++ b/tests/pox5/regtest-env/stacking/package.json @@ -0,0 +1,41 @@ +{ + "name": "stacking", + "version": "1.0.0", + "description": "", + "type": "module", + "scripts": { + "format": "prettier --write *.ts", + "typecheck": "tsc --noEmit" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@clarigen/core": "^4.1.6", + "@noble/curves": "^2.0.1", + "@scure/base": "^1.2.0", + "@scure/btc-signer": "^1.5.0", + "@stacks/api": "6.11.4-pr.472091f.0", + "@stacks/blockchain-api-client": "8.15.1", + "@stacks/clarinet-sdk": "^3.18.1", + "@stacks/common": "7.3.1", + "@stacks/encryption": "7.4.0", + "@stacks/network": "7.3.1", + "@stacks/stacking": "7.4.0", + "@stacks/stacks-blockchain-api-types": "7.8.2", + "@stacks/transactions": "7.4.0", + "dotenv": "^16.4.5", + "pino": "^8.19.0", + "pino-pretty": "^10.3.1" + }, + "devDependencies": { + "@stacks/prettier-config": "^0.0.10", + "@total-typescript/tsconfig": "^1.0.4", + "tsx": "4.7.1", + "typescript": "^6.0.2" + }, + "prettier": "@stacks/prettier-config", + "resolutions": { + "@stacks/network": "7.4.0" + } +} diff --git a/tests/pox5/regtest-env/stacking/pox-5-helpers.ts b/tests/pox5/regtest-env/stacking/pox-5-helpers.ts new file mode 100644 index 0000000000..4245d24781 --- /dev/null +++ b/tests/pox5/regtest-env/stacking/pox-5-helpers.ts @@ -0,0 +1,128 @@ +import * as BTC from '@scure/btc-signer'; +import { + Cl, + createAddress, + encodeStructuredDataBytes, + getAddressFromPublicKey, + signWithKey, +} from '@stacks/transactions'; +import { hex } from '@scure/base'; +import { + ClarigenClient, + contractFactory, + projectErrors, + TESTNET_BURN_ADDRESS, +} from '@clarigen/core'; +import { contracts, project } from './clarigen-types.js'; +import { sha256 } from '@noble/hashes/sha2.js'; +import { network } from './common.js'; + +export const clarigenClient = new ClarigenClient(network); + +export const pox5 = contractFactory(contracts.pox5, `${TESTNET_BURN_ADDRESS}.pox-5`); +export const pox5Signer = (contractAddress: string) => + contractFactory(contracts.pox5Signer, contractAddress); + +export const errorCodes = projectErrors(project).pox5; + +export function toWitnessOutput(script: Uint8Array) { + return BTC.OutScript.encode( + BTC.p2wsh({ + type: 'wsh', + script, + }) + ); +} + +export function serializeLockupScript({ + stacker, + unlockBurnHeight, + unlockBytes, +}: { + stacker: string; + unlockBurnHeight: bigint; + unlockBytes: Uint8Array; +}) { + const addr = createAddress(stacker); + return BTC.Script.encode([ + new Uint8Array([5, addr.version, ...hex.decode(addr.hash160)]), + 'DROP', + Number(unlockBurnHeight), + 'CHECKLOCKTIMEVERIFY', + 'DROP', + unlockBytes, + ]); +} + +export function signSignerKeyGrant({ + signerManager, + authId, + signerSk, +}: { + signerManager: string; + authId: bigint; + signerSk: Uint8Array; +}) { + const message = Cl.tuple({ + 'signer-manager': Cl.principal(signerManager), + topic: Cl.stringAscii('grant-authorization'), + 'auth-id': Cl.uint(authId), + }); + const fullMessage = encodeStructuredDataBytes({ + message, + domain: Cl.tuple({ + name: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.name), + version: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.version), + 'chain-id': Cl.uint(pox5.constants.pOX_5_SIGNER_DOMAIN.chainId), + }), + }); + const data = signWithKey(signerSk, hex.encode(sha256(fullMessage))); + const signature = hex.decode(data.slice(2) + data.slice(0, 2)); + return signature; +} + +/** Get the testnet STX address for a signer key. */ +export function signerAddress(signerKey: Uint8Array) { + return getAddressFromPublicKey(signerKey, 'testnet'); +} + +/** Sign a per-transaction signer authorization (the signer-sig path). */ +export function signPerTransactionAuth({ + signerSk, + poxAddr, + rewardCycle, + topic, + period, + maxAmount, + authId, +}: { + signerSk: Uint8Array; + poxAddr: { version: Uint8Array; hashbytes: Uint8Array }; + rewardCycle: bigint; + topic: string; + period: bigint | number; + maxAmount: bigint | number; + authId: bigint | number; +}) { + const message = Cl.tuple({ + 'pox-addr': Cl.tuple({ + version: Cl.buffer(poxAddr.version), + hashbytes: Cl.buffer(poxAddr.hashbytes), + }), + 'reward-cycle': Cl.uint(rewardCycle), + topic: Cl.stringAscii(topic), + period: Cl.uint(period), + 'auth-id': Cl.uint(authId), + 'max-amount': Cl.uint(maxAmount), + }); + const fullMessage = encodeStructuredDataBytes({ + message, + domain: Cl.tuple({ + name: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.name), + version: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.version), + 'chain-id': Cl.uint(pox5.constants.pOX_5_SIGNER_DOMAIN.chainId), + }), + }); + const data = signWithKey(signerSk, hex.encode(sha256(fullMessage))); + return hex.decode(data.slice(2) + data.slice(0, 2)); +} diff --git a/tests/pox5/regtest-env/stacking/stacking.ts b/tests/pox5/regtest-env/stacking/stacking.ts new file mode 100644 index 0000000000..e3aca00bb5 --- /dev/null +++ b/tests/pox5/regtest-env/stacking/stacking.ts @@ -0,0 +1,216 @@ +import { PoxInfo, Pox4SignatureTopic } from '@stacks/stacking'; +import crypto from 'crypto'; +import { + Account, + accounts, + maxAmount, + parseEnvInt, + waitForSetup, + logger, + burnBlockToRewardCycle, +} from './common.js'; + +const randInt = () => crypto.randomInt(0, 0xffffffffffff); +const stackingInterval = parseEnvInt('STACKING_INTERVAL', true); +const postTxWait = parseEnvInt('POST_TX_WAIT', true); +const stackingCycles = parseEnvInt('STACKING_CYCLES', true); +const stackAmount = parseEnvInt('STACK_AMOUNT_STX', false); +const stackingFee = parseEnvInt('STACKING_FEE', false) ?? 1000000; + +let startTxFee = stackingFee; +const getNextTxFee = () => startTxFee++; + +async function run() { + const poxInfo = await accounts[0]!.client.getPoxInfo(); + if (!poxInfo.contract_id.endsWith('.pox-4')) { + // console.log(`Pox contract is not .pox-4, skipping stacking (contract=${poxInfo.contract_id})`); + logger.info( + { + poxContract: poxInfo.contract_id, + }, + `Pox contract is not .pox-4, skipping stacking (contract=${poxInfo.contract_id})` + ); + return; + } + + const runLog = logger.child({ + burnHeight: poxInfo.current_burnchain_block_height, + }); + + const accountInfos = await Promise.all( + accounts.map(async a => { + const info = await a.client.getAccountStatus(); + const unlockHeight = Number(info.unlock_height); + const lockedAmount = BigInt(info.locked); + const balance = BigInt(info.balance); + return { ...a, info, unlockHeight, lockedAmount, balance }; + }) + ); + + let txSubmitted = false; + + // Bump min threshold by 50% to avoid getting stuck if threshold increases + const minStx = Math.floor(poxInfo.next_cycle.min_threshold_ustx * 1.5); + const nextCycleStx = poxInfo.next_cycle.stacked_ustx; + if (nextCycleStx < minStx) { + runLog.info(`Next cycle has less than min threshold.. stacking should be performed soon`); + } + + await Promise.all( + accountInfos.map(async account => { + if (account.lockedAmount === 0n) { + runLog.info( + { + burnHeight: poxInfo.current_burnchain_block_height, + unlockHeight: account.unlockHeight, + account: account.index, + }, + `Account ${account.index} is unlocked, stack-stx required` + ); + await stackStx(poxInfo, account, account.balance); + txSubmitted = true; + return; + } + const unlockHeightCycle = burnBlockToRewardCycle(account.unlockHeight); + const nowCycle = burnBlockToRewardCycle(poxInfo.current_burnchain_block_height ?? 0); + if (unlockHeightCycle === nowCycle + 1) { + runLog.info( + { + burnHeight: poxInfo.current_burnchain_block_height, + unlockHeight: account.unlockHeight, + account: account.index, + nowCycle, + unlockCycle: unlockHeightCycle, + }, + `Account ${account.index} unlocks before next cycle ${account.unlockHeight} vs ${poxInfo.current_burnchain_block_height}, stack-extend required` + ); + await stackExtend(poxInfo, account); + txSubmitted = true; + return; + } + runLog.info( + { + burnHeight: poxInfo.current_burnchain_block_height, + unlockHeight: account.unlockHeight, + account: account.index, + nowCycle, + unlockCycle: unlockHeightCycle, + }, + `Account ${account.index} is locked for next cycle, skipping stacking` + ); + }) + ); + + if (txSubmitted) { + await new Promise(resolve => setTimeout(resolve, postTxWait * 1000)); + } +} + +async function stackStx(poxInfo: PoxInfo, account: Account, balance: bigint) { + // Bump min threshold by 50% to avoid getting stuck if threshold increases + const minStx = Math.floor(poxInfo.next_cycle.min_threshold_ustx * 1.5); + let amountToStx = BigInt(minStx) * BigInt(account.targetSlots); + if (typeof stackAmount === 'number') { + amountToStx = BigInt(stackAmount) * 1_000_000n; + } + + if (amountToStx > balance) { + throw new Error( + `Insufficient balance to stack-stx (amount=${amountToStx}, balance=${balance})` + ); + } + const authId = randInt(); + const sigArgs = { + topic: Pox4SignatureTopic.StackStx, + rewardCycle: poxInfo.reward_cycle_id, + poxAddress: account.btcAddr, + period: stackingCycles, + signerPrivateKey: account.signerPrivKey, + authId, + maxAmount, + } as const; + const signerSignature = account.client.signPoxSignature(sigArgs); + const stackingArgs = { + poxAddress: account.btcAddr, + privateKey: account.privKey, + amountMicroStx: amountToStx, + burnBlockHeight: poxInfo.current_burnchain_block_height, + cycles: stackingCycles, + fee: getNextTxFee(), + signerKey: account.signerPubKey, + signerSignature, + authId, + maxAmount, + }; + const { signerPrivateKey, ...restSigArgs } = sigArgs; + account.logger.debug( + { + ...stackingArgs, + ...restSigArgs, + }, + `Stack-stx with args:` + ); + const stackResult = await account.client.stack(stackingArgs); + account.logger.info( + { + ...stackResult, + }, + `Stack-stx tx result` + ); +} + +async function stackExtend(poxInfo: PoxInfo, account: Account) { + const authId = randInt(); + const sigArgs = { + topic: Pox4SignatureTopic.StackExtend, + rewardCycle: poxInfo.reward_cycle_id, + poxAddress: account.btcAddr, + period: stackingCycles, + signerPrivateKey: account.signerPrivKey, + authId, + maxAmount, + } as const; + const signerSignature = account.client.signPoxSignature(sigArgs); + const stackingArgs = { + poxAddress: account.btcAddr, + privateKey: account.privKey, + extendCycles: stackingCycles, + fee: getNextTxFee(), + signerKey: account.signerPubKey, + signerSignature, + authId, + maxAmount, + }; + const { signerPrivateKey, ...restSigArgs } = sigArgs; + account.logger.debug( + { + stxAddress: account.stxAddress, + account: account.index, + ...stackingArgs, + ...restSigArgs, + }, + `Stack-extend with args:` + ); + const stackResult = await account.client.stackExtend(stackingArgs); + account.logger.info( + { + stxAddress: account.stxAddress, + account: account.index, + ...stackResult, + }, + `Stack-extend tx result` + ); +} + +async function loop() { + await waitForSetup(); + while (true) { + try { + await run(); + } catch (e) { + console.error('Error running stacking:', e); + } + await new Promise(resolve => setTimeout(resolve, stackingInterval * 1000)); + } +} +loop(); diff --git a/tests/pox5/regtest-env/stacking/tsconfig.json b/tests/pox5/regtest-env/stacking/tsconfig.json new file mode 100644 index 0000000000..c9735b6dfb --- /dev/null +++ b/tests/pox5/regtest-env/stacking/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@total-typescript/tsconfig/tsc/no-dom/library", + "compilerOptions": { + "verbatimModuleSyntax": false, + } +} \ No newline at end of file diff --git a/tests/pox5/regtest-env/stacking/tx-broadcaster.ts b/tests/pox5/regtest-env/stacking/tx-broadcaster.ts new file mode 100644 index 0000000000..055ea0d101 --- /dev/null +++ b/tests/pox5/regtest-env/stacking/tx-broadcaster.ts @@ -0,0 +1,117 @@ +import { StackingClient } from '@stacks/stacking'; +import { + getAddressFromPrivateKey, + makeSTXTokenTransfer, + broadcastTransaction, + StacksTransactionWire, + fetchNonce +} from '@stacks/transactions'; +import { logger, network } from './common.js'; + +const broadcastInterval = parseInt(process.env.NAKAMOTO_BLOCK_INTERVAL ?? '2'); +const url = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env.STACKS_CORE_RPC_PORT}`; +const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0'); + +const accounts = process.env.ACCOUNT_KEYS!.split(',').map(privKey => ({ + privKey, + stxAddress: getAddressFromPrivateKey(privKey, network), +})); + +const client = new StackingClient({ + address: accounts[0]!.stxAddress, + network, +}); + +async function run() { + const accountNonces = await Promise.all( + accounts.map(async account => { + const nonce = await fetchNonce({ + address: account.stxAddress, + network, + }); + return { ...account, nonce }; + }) + ); + + // Send from account with lowest nonce + accountNonces.sort((a, b) => Number(a.nonce) - Number(b.nonce)); + const sender = accountNonces[0]!; + const recipient = accountNonces[1]!; + + logger.info( + `Sending stx-transfer from ${sender.stxAddress} (nonce=${sender.nonce}) to ${recipient.stxAddress}` + ); + + const tx = await makeSTXTokenTransfer({ + recipient: recipient.stxAddress, + amount: 1000, + senderKey: sender.privKey, + network, + nonce: sender.nonce, + fee: 300, + }); + await broadcast(tx, sender.stxAddress); +} + +async function broadcast(tx: StacksTransactionWire, sender?: string) { + const txType = tx.payload.payloadType; + const label = sender ? accountLabel(sender) : 'Unknown'; + const broadcastResult = await broadcastTransaction({ + transaction: tx, + network, + }); + if ('error' in broadcastResult) { + logger.error({ ...broadcastResult, account: label }, `Error broadcasting ${txType}`); + return false; + } else { + if (label.includes('Flooder')) return true; + logger.debug(`Broadcast ${txType} from ${label} tx=${broadcastResult.txid}`); + return true; + } +} + +async function waitForNakamoto() { + while (true) { + try { + const poxInfo = await client.getPoxInfo(); + if (poxInfo.current_burnchain_block_height! <= EPOCH_30_START) { + logger.info( + `Nakamoto not activated yet, waiting... (current=${poxInfo.current_burnchain_block_height}), (epoch3=${EPOCH_30_START})` + ); + } else { + logger.info( + `Nakamoto activation height reached, ready to submit txs for Nakamoto block production` + ); + break; + } + } catch (error) { + if (error instanceof Error && 'cause' in error && error.cause instanceof Error && /(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause.message)) { + logger.info(`Stacks node not ready, waiting...`); + } else { + logger.error('Error getting pox info:', error); + } + } + await new Promise(resolve => setTimeout(resolve, 3000)); + } +} + +function accountLabel(address: string) { + const accountIndex = accounts.findIndex(account => account.stxAddress === address); + if (accountIndex !== -1) { + return `Account #${accountIndex}`; + } + return `Unknown (${address})`; +} + +async function loop() { + await waitForNakamoto(); + while (true) { + try { + await run(); + } catch (e) { + logger.error(e, 'Error in tx-broadcaster loop'); + } + await new Promise(resolve => setTimeout(resolve, broadcastInterval * 1000)); + } +} +loop(); diff --git a/tests/pox5/regtest-env/stacks-krypton-miner.toml b/tests/pox5/regtest-env/stacks-krypton-miner.toml new file mode 100644 index 0000000000..4c336bd62a --- /dev/null +++ b/tests/pox5/regtest-env/stacks-krypton-miner.toml @@ -0,0 +1,209 @@ +[node] +name = "krypton-node" +rpc_bind = "0.0.0.0:20443" +p2p_bind = "0.0.0.0:20444" +data_url = "http://127.0.0.1:20443" +p2p_address = "127.0.0.1:20443" +working_dir = "$DATA_DIR" + +seed = "$MINER_SEED" +local_peer_seed = "$MINER_SEED" + +miner = true +use_test_genesis_chainstate = true +pox_sync_sample_secs = 0 +wait_time_for_blocks = 0 +wait_time_for_microblocks = 0 +mine_microblocks = false +microblock_frequency = 1000 +# mine_microblocks = true +# max_microblocks = 10 +pox_5_sbtc_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-token" +pox_5_sbtc_registry_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-registry" +pox_5_bond_admin = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP" + +[miner] +first_attempt_time_ms = 180_000 +subsequent_attempt_time_ms = 360_000 +microblock_attempt_time_ms = 10 +#self_signing_seed = 1 +mining_key = "19ec1c3e31d139c989a23a27eac60d1abfad5277d3ae9604242514c738258efa01" +$REWARD_RECIPIENT_CONF + +[connection_options] +# inv_sync_interval = 10 +# download_interval = 10 +# walk_interval = 10 +disable_block_download = true +disable_inbound_handshakes = false +disable_inbound_walks = false +public_ip_address = "1.1.1.1:1234" +auth_token = "12345" +read_only_call_limit_read_length = 10000000000 + +# Add stacks-api as an event observer. +# Points at the Stacks API running manually on the host instead of the dockerized service. +[[events_observer]] +endpoint = "host.docker.internal:3700" +events_keys = ["*"] + +# Add stacks-signer as an event observer +[[events_observer]] +endpoint = "stacks-signer-1:30001" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +[[events_observer]] +endpoint = "stacks-signer-2:30002" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +[[events_observer]] +endpoint = "stacks-signer-3:30003" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +[burnchain] +chain = "bitcoin" +mode = "krypton" +poll_time_secs = 1 +magic_bytes = "T3" +pox_prepare_length = $POX_PREPARE_LENGTH +pox_reward_length = $POX_REWARD_LENGTH +burn_fee_cap = 20_000 +chain_id = $STACKS_CHAIN_ID +### bitcoind-regtest connection info +peer_host = "$BITCOIN_PEER_HOST" +peer_port = $BITCOIN_PEER_PORT +rpc_port = $BITCOIN_RPC_PORT +rpc_ssl = false +username = "$BITCOIN_RPC_USER" +password = "$BITCOIN_RPC_PASS" +timeout = 30 +wallet_name = "stx_miner_0" + +[[burnchain.epochs]] +epoch_name = "1.0" +start_height = 0 + +[[burnchain.epochs]] +epoch_name = "2.0" +start_height = $STACKS_20_HEIGHT + +[[burnchain.epochs]] +epoch_name = "2.05" +start_height = $STACKS_2_05_HEIGHT + +[[burnchain.epochs]] +epoch_name = "2.1" +start_height = $STACKS_21_HEIGHT + +[[burnchain.epochs]] +epoch_name = "2.2" +start_height = $STACKS_22_HEIGHT + +[[burnchain.epochs]] +epoch_name = "2.3" +start_height = $STACKS_23_HEIGHT + +[[burnchain.epochs]] +epoch_name = "2.4" +start_height = $STACKS_24_HEIGHT + +[[burnchain.epochs]] +epoch_name = "2.5" +start_height = $STACKS_25_HEIGHT + +[[burnchain.epochs]] +epoch_name = "3.0" +start_height = $STACKS_30_HEIGHT + +[[burnchain.epochs]] +epoch_name = "3.1" +start_height = $STACKS_31_HEIGHT + +[[burnchain.epochs]] +epoch_name = "3.2" +start_height = $STACKS_32_HEIGHT + +[[burnchain.epochs]] +epoch_name = "3.3" +start_height = $STACKS_33_HEIGHT + +[[burnchain.epochs]] +epoch_name = "3.4" +start_height = $STACKS_34_HEIGHT + +[[burnchain.epochs]] +epoch_name = "4.0" +start_height = $STACKS_40_HEIGHT + + +[[ustx_balance]] +address = "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6" +amount = 10000000000000000 +# secretKey = "cb3df38053d132895220b9ce471f6b676db5b9bf0b4adefb55f2118ece2478df01" + +[[ustx_balance]] +address = "ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y" +amount = 10000000000000000 +# secretKey = "21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb752601" + +[[ustx_balance]] +address = "ST1HB1T8WRNBYB0Y3T7WXZS38NKKPTBR3EG9EPJKR" +amount = 10000000000000000 +# Account keys 3 +# secretKey = "c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01" + +[[ustx_balance]] +address = "ST2PGGD0ZXAWEMY4EZ025RD1X47EEVH287SQKA8BC" +amount = 10000000000000000 +# Account keys 2 +# secretKey = "975b251dd7809469ef0c26ec3917971b75c51cd73a022024df4bf3b232cc2dc001" + +[[ustx_balance]] +address = "ST29V10QEA7BRZBTWRFC4M70NJ4J6RJB5P1C6EE84" +amount = 10000000000000000 +# Account keys 1 +# secretKey = "0d2f965b472a82efd5a96e6513c8b9f7edc725d5c96c7d35d6c722cedeb80d1b01" + +# Stacker/signer +[[ustx_balance]] +address = "ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0" +amount = 10000000000000000 +# secret_key: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 +# mnemonic = "area desk dutch sign gold cricket dawn toward giggle vibrant indoor bench warfare wagon number tiny universe sand talk dilemma pottery bone trap buddy" +# stx_address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 +# btc_address: mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt + +# Stacker/signer +[[ustx_balance]] +address = "ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ" +amount = 10000000000000000 +# secret_key: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 +# mnemonic = "prevent gallery kind limb income control noise together echo rival record wedding sense uncover school version force bleak nuclear include danger skirt enact arrow" +# stx_address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ +# btc_address: n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7 + +# Stacker/signer +[[ustx_balance]] +address = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP" +amount = 10000000000000000 +# secret_key: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 +# mnemonic = "female adjust gallery certain visit token during great side clown fitness like hurt clip knife warm bench start reunion globe detail dream depend fortune" +# stx_address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP +# btc_address: n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw + +[[ustx_balance]] +address = "ST5B3TD6YF085JWKSSW9HDWCDZTR842RFNP19HQC" +amount = 10000000000000000 +# used in "flood.ts" +# secretKey = 66b7a77a3e0abc2cddaa51ed38fc4553498e19d3620ef08eb141afcfd0e3f5b501 + +[[ustx_balance]] +address = "STEH2J3C05BAHYS0RBAQBANJ1AXR6SR43VMZ0D49" +amount = 10000000000000000 +# secretKey = 5b8303150239eceaba43892af7cdd1fa7fc26eda5182ebaaa568e3341d54a4d001 + +[[ustx_balance]] +address = "STT8DSJTWAW9TVJ1B17SD3S6F7SYH4TXG7TWS7Q9" +amount = 10000000000000000 +# privateKey = 16226f674796712dfbd53bf402304579b8b6d04d4bed4d466bf84ce6db973d4401 +# mnemonic = "essay grief twin tube concert idea prosper good alarm goddess shell glare hurt belt endless patch lumber wrap labor body erupt brown style test" diff --git a/tests/pox5/setup.ts b/tests/pox5/setup.ts new file mode 100644 index 0000000000..a0c1e3c45e --- /dev/null +++ b/tests/pox5/setup.ts @@ -0,0 +1,65 @@ +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + dockerComposeTestDown, + dockerComposeTestUp, + type DockerComposeTestConfig, +} from '@stacks/api-test-toolkit'; + +/** + * Global setup/teardown for the pox-5 + bitcoin-staking e2e suite. + * + * Unlike the `krypton` and `snp` suites (which boot single pre-built images via + * `dockerTestUp`), the pox-5 environment needs the full multi-service + * `docker compose` stack copied from `stacks-regtest-env`: + * + * - bitcoind + bitcoind-miner regtest BTC node, auto-mines blocks, funds the + * `btc_staking` wallet used by the btc-staker + * - stacks-node Nakamoto/epoch-4 miner, built from the + * `feat/epoch-4-rc` stacks-blockchain commit + * - stacks-signer-{1,2,3} the signer set + * - stacker pox-4 auto-stacker (talks to the node directly) + * - btc-staker pox-5 bitcoin-staking + sbtc deployer + * - monitor / tx-broadcaster chain progression + nakamoto block production + * - postgres chainstate DB for the API + * + * The Stacks API itself is intentionally NOT containerized: the miner posts + * events to `host.docker.internal:3700` and the btc-staker/monitor query the + * API at `host.docker.internal:3999`. The API + event server are started + * in-process by the test files (see the upcoming pox5-env helper), exactly the + * way `krypton-env.ts` does it. This setup only owns the docker lifecycle. + */ +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function pox5ComposeConfig(): DockerComposeTestConfig { + return { + projectName: 'stacks-api-test-pox5', + composeFile: join(__dirname, 'regtest-env', 'docker-compose.yml'), + // Run docker quietly (capture output, surface only on failure) so the + // console stays focused on the test runner. Heartbeat lines report progress + // during the build/boot/readiness waits; set DEBUG_COMPOSE=1 to stream the + // full docker output instead. + inheritStdio: process.env.DEBUG_COMPOSE === '1', + // Only wait for postgres here — NOT the stacks-node. + // + // The node posts events to the in-process event server at host:3700 and + // makes no chain progress until that server is up and draining events. The + // event server is started later by `getPox5Context()` (in the test's + // before-hook), so gating container readiness on node/chain progress here + // would deadlock: setup waits on the node, the node waits on an event server + // that setup never lets us start. Node readiness is therefore checked inside + // `getPox5Context()`, after the event server is listening. Postgres comes up + // independently and is needed before the API runs migrations. + waitFor: [{ type: 'port', port: 5490, label: 'postgres' }], + }; +} + +export async function globalSetup() { + await dockerComposeTestUp({ config: pox5ComposeConfig() }); + process.stdout.write(`[testenv:pox5] all containers ready\n`); +} + +export async function globalTeardown() { + await dockerComposeTestDown({ config: pox5ComposeConfig() }); + process.stdout.write(`[testenv:pox5] all containers removed\n`); +} diff --git a/tests/pox5/smoke.test.ts b/tests/pox5/smoke.test.ts new file mode 100644 index 0000000000..6d58b2efdb --- /dev/null +++ b/tests/pox5/smoke.test.ts @@ -0,0 +1,33 @@ +import { after, before, describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { getPox5Context, stopPox5Context, type Pox5Context } from './pox5-env.ts'; + +/** + * End-to-end harness smoke test. + * + * Exercises the full pipeline without asserting any chain behavior yet: + * 1. `setup.ts` (global) has already booted the dockerized chain + postgres. + * 2. `getPox5Context()` starts the in-process API + event server and connects + * to the dockerized node/bitcoind/postgres. + * 3. A trivial assertion confirms the suite runs green start-to-finish. + * + * Once this passes reliably, real pox-5 / bitcoin-staking assertions get added + * as sibling `*.test.ts` files using the same context + `standByFor*` helpers. + */ +describe('pox5 e2e harness smoke test', () => { + let ctx: Pox5Context; + + before(async () => { + ctx = await getPox5Context(); + }); + + after(async () => { + if (ctx) { + await stopPox5Context(ctx); + } + }); + + test('harness boots and the suite runs end-to-end', () => { + assert.ok(true); + }); +});