diff --git a/.gitignore b/.gitignore index ef5738c..f0d8cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ stacking/node_modules package-lock.json +.cache +node_modules +ai \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..77eea67 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "biome.enabled": false +} diff --git a/Dockerfile.stacker b/Dockerfile.stacker index 83e84cc..47c2c70 100644 --- a/Dockerfile.stacker +++ b/Dockerfile.stacker @@ -6,6 +6,10 @@ WORKDIR /root COPY ./stacking/package.json /root/ RUN npm i -COPY ./stacking/stacking.ts ./stacking/common.ts ./stacking/monitor.ts ./stacking/tx-broadcaster.ts /root/ +COPY ./stacking/deployments/*.yaml /root/deployments/ +COPY ./stacking/contracts/*.clar /root/contracts/ +COPY ./stacking/*.ts /root/ -CMD ["npx", "tsx", "/root/stacking.ts"] \ No newline at end of file +ENV NODE_OPTIONS=--enable-source-maps + +CMD ["./node_modules/.bin/tsx", "/root/stacking.ts"] diff --git a/Dockerfile.stacks-api b/Dockerfile.stacks-api index 61dadd9..8757a5b 100644 --- a/Dockerfile.stacks-api +++ b/Dockerfile.stacks-api @@ -12,7 +12,7 @@ RUN git init && \ git reset --hard FETCH_HEAD && \ git fetch --all --tags -RUN rm ".env" +# RUN rm ".env" RUN git describe --tags --abbrev=0 || git -c user.name='user' -c user.email='email' tag vNext RUN echo "GIT_TAG=$(git tag --points-at HEAD)" >> .env diff --git a/Dockerfile.stacks-node b/Dockerfile.stacks-node index b6ef52b..f7b7bb8 100644 --- a/Dockerfile.stacks-node +++ b/Dockerfile.stacks-node @@ -1,27 +1,28 @@ FROM rust:bookworm AS builder -# TODO: is there a built-in required arg syntax? -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" - 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 cargo build --package stacks-node --package stacks-signer --bin stacks-node --bin stacks-signer +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 /stacks/target/debug/stacks-node /usr/local/bin/ -COPY --from=builder /stacks/target/debug/stacks-signer /usr/local/bin/ +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 diff --git a/docker-compose.yml b/docker-compose.yml index 6b262cc..8f815c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,6 @@ -version: "3.9" - x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 1cba695b09690790b1220b0f5d149f70f78ddea5 # 3.3.0.0.1 - - &STACKS_API_COMMIT 7ee1adbddac269d0c03567677013470a62e92f99 # 8.13.4 + - &STACKS_BLOCKCHAIN_COMMIT 27877974d3c9a0daea20ec7204cb8d7e7aa95e83 # 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 @@ -10,8 +8,9 @@ x-common-vars: - &BITCOIN_RPC_USER btc - &BITCOIN_RPC_PASS btc - &MINE_INTERVAL ${MINE_INTERVAL:-1s} - - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-5s} # 5 second bitcoin block times in epoch 2.5 - - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-30s} # 30 second bitcoin block times in epoch 3 + - &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} @@ -25,7 +24,10 @@ x-common-vars: - &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 @@ -75,9 +77,11 @@ services: 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 @@ -142,9 +146,20 @@ services: 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 @@ -190,6 +205,15 @@ services: 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 @@ -205,7 +229,10 @@ services: SLEEP_DURATION=$${MINE_INTERVAL} BLOCK_HEIGHT=$$(bitcoin-cli -rpcconnect=bitcoind getblockcount) - if [ "$${BLOCK_HEIGHT}" -ge $$(( $${STACKS_30_HEIGHT} - 1 )) ]; then + 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 @@ -256,10 +283,13 @@ services: 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 @@ -285,18 +315,58 @@ services: 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 + environment: + STACKS_CORE_RPC_HOST: stacks-api + 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 + monitor: networks: - stacks @@ -307,14 +377,17 @@ services: STACKS_CORE_RPC_HOST: stacks-api 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 EXIT_FROM_MONITOR: *EXIT_FROM_MONITOR STACKS_CHAIN_ID: *STACKS_CHAIN_ID SERVICE_NAME: monitor + NODE_OPTIONS: --enable-source-maps depends_on: - stacks-node entrypoint: @@ -322,7 +395,7 @@ services: - -c - | set -e - exec npx tsx /root/monitor.ts + exec ./node_modules/.bin/tsx /root/monitor.ts tx-broadcaster: networks: @@ -337,10 +410,12 @@ services: 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: @@ -348,12 +423,13 @@ services: - -c - | set -e - exec npx tsx /root/tx-broadcaster.ts + exec ./node_modules/.bin/tsx /root/tx-broadcaster.ts postgres: networks: - stacks image: "postgres:15" + pull_policy: if_not_present ports: - "5490:5490" volumes: @@ -394,6 +470,7 @@ services: PG_DATABASE: stacks_blockchain_api PG_SCHEMA: public STACKS_CORE_RPC_HOST: stacks-node + STACKS_CORE_PROXY_HOST: stacks-node STACKS_CORE_RPC_PORT: 20443 BTC_RPC_HOST: http://bitcoind BTC_RPC_PORT: 18443 diff --git a/run.sh b/run.sh index 9f21744..a013ca9 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -docker compose down --volumes --remove-orphans --timeout=1 --rmi=all +docker compose down --volumes --remove-orphans --timeout=1 --rmi=local # docker compose up --build -docker compose up --build --exit-code-from monitor \ No newline at end of file +docker compose up --build \ No newline at end of file diff --git a/stacking/Clarigen.toml b/stacking/Clarigen.toml new file mode 100644 index 0000000..3d0eea4 --- /dev/null +++ b/stacking/Clarigen.toml @@ -0,0 +1,30 @@ + +# Set to your project's Clarinet config file +clarinet = "./Clarinet.toml" + +# Set where you'd like TypeScript types output. +# Comment or remove section to skip TypeScript types +[types] +# `output` should be a path to a single file +output = "clarigen-types.ts" + +# You can also specify multiple output paths: +# outputs = [ +# "src/clarigen-types.ts", +# "test/clarigen-types.ts" +# ] + +# `types.after` - script to run after TypeScript types are generated. +# examples: +# after = "npm run prettier -w ./src/clarigen-types.ts" +# after = "echo 'yay'" + +# Set where you'd like generated contract docs +# Generate docs by running `clarigen docs` +[docs] +# `output` should be a folder +output = "docs" + +# `docs.after` - script to run after docs are generated. +# examples: +# after = "npm run prettier -w ./docs" diff --git a/stacking/Clarinet.toml b/stacking/Clarinet.toml new file mode 100644 index 0000000..5001815 --- /dev/null +++ b/stacking/Clarinet.toml @@ -0,0 +1,24 @@ + +[project] +name = "core-contracts" + +[[project.requirements]] +contract_id = "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal" + +[repl] +costs_version = 1 + +[contracts.pox-5] +path = "./contracts/pox-5.clar" +clarity_version = 4 +epoch = 3.3 + +[contracts.pox-5-signer] +path = "./contracts/pox-5-signer.clar" +clarity_version = 4 +epoch = 3.3 + +[repl.analysis.lints] +unused_const = "warn" +unused_data_var = "warn" +panic = "off" diff --git a/stacking/btc-helpers.ts b/stacking/btc-helpers.ts new file mode 100644 index 0000000..b24861c --- /dev/null +++ b/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/stacking/btc-staker.ts b/stacking/btc-staker.ts new file mode 100644 index 0000000..c669cec --- /dev/null +++ b/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/stacking/clarigen-types.ts b/stacking/clarigen-types.ts new file mode 100644 index 0000000..5b51fb5 --- /dev/null +++ b/stacking/clarigen-types.ts @@ -0,0 +1,1844 @@ + +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>>, + crystallizeRewards: {"name":"crystallize-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], { + "earned": bigint; + "rewardsPerToken": bigint; +}>, + getBitcoinTxOutput_q: {"name":"get-bitcoin-tx-output?","access":"private","args":[{"name":"tx-bytes","type":{"buffer":{"length":100000}}},{"name":"output-index","type":"uint128"},{"name":"amount","type":"uint128"},{"name":"script","type":{"buffer":{"length":34}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount","type":"uint128"},{"name":"script","type":{"buffer":{"length":34}}},{"name":"txid","type":{"buffer":{"length":32}}}]},"error":"uint128"}}}} as TypedAbiFunction<[txBytes: TypedAbiArg, outputIndex: TypedAbiArg, amount: TypedAbiArg, script: TypedAbiArg], Response<{ + "amount": bigint; + "script": Uint8Array; + "txid": Uint8Array; +}, 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>, + 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":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: 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>, + verifyMerkleProof: {"name":"verify-merkle-proof","access":"private","args":[{"name":"leaf-hash","type":{"buffer":{"length":32}}},{"name":"root-hash","type":{"buffer":{"length":32}}},{"name":"tx-index","type":"uint128"},{"name":"tx-count","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}}],"outputs":{"type":"bool"}} as TypedAbiFunction<[leafHash: TypedAbiArg, rootHash: TypedAbiArg, txIndex: TypedAbiArg, txCount: TypedAbiArg, leafHashes: TypedAbiArg], boolean>, + 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-signers","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-signers","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, earlyUnlockSigners: TypedAbiArg, earlyUnlockAdmin: TypedAbiArg, allowlist: TypedAbiArg<{ + "maxSats": number | bigint; + "staker": string; +}[], "allowlist">], Response<{ + "bondIndex": bigint; + "earlyUnlockSigners": 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":5141}}}} 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":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], 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-signers","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; + "earlyUnlockSigners": 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":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: 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>, + getSignerPendingRewardsForCycle: {"name":"get-signer-pending-rewards-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + 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>, + getSignerRewardsPerTokenPaidForCycle: {"name":"get-signer-rewards-per-token-paid-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: 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":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: 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":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: 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":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: 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-signers","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>, + signerPendingRewardsForCycle: {"name":"signer-pending-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>, + 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>, + signerRewardsPerTokenPaidForCycle: {"name":"signer-rewards-per-token-paid-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>, + 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>, + usedSignerKeyAuthorizations: {"name":"used-signer-key-authorizations","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"period","type":"uint128"},{"name":"pox-addr","type":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}},{"name":"reward-cycle","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"topic","type":{"string-ascii":{"length":14}}}]},"value":"bool"} as TypedAbiMap<{ + "authId": number | bigint; + "maxAmount": number | bigint; + "period": number | bigint; + "poxAddr": { + "hashbytes": Uint8Array; + "version": Uint8Array; +} | null; + "rewardCycle": number | bigint; + "signerKey": Uint8Array; + "topic": string; +}, boolean>, + 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_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_POX_ADDRESS: { + name: 'ERR_INVALID_POX_ADDRESS', + 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_L1_LOCKUP_NOT_FOUND: { + name: 'ERR_L1_LOCKUP_NOT_FOUND', + 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_NO_SBTC_BALANCE: { + name: 'ERR_NO_SBTC_BALANCE', + 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_AUTH_AMOUNT_TOO_HIGH: { + name: 'ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_AUTH_USED: { + name: 'ERR_SIGNER_AUTH_USED', + 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_POX_ADDR_MISMATCH: { + name: 'ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH', + 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_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, + 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_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_POX_ADDRESS: { + isOk: false, + value: 21n + }, + 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_L1_LOCKUP_NOT_FOUND: { + isOk: false, + value: 6n + }, + 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_NO_SBTC_BALANCE: { + isOk: false, + value: 25n + }, + ERR_READ_TX_OUT_OF_BOUNDS: { + isOk: false, + value: 39n + }, + ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH: { + isOk: false, + value: 15n + }, + ERR_SIGNER_AUTH_USED: { + isOk: false, + value: 16n + }, + ERR_SIGNER_KEY_GRANT_NOT_FOUND: { + isOk: false, + value: 17n + }, + ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH: { + isOk: false, + value: 18n + }, + 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_ADDRESS_VERSION: 6n, + mAX_ADDRESS_VERSION_BUFF_20: 4n, + mAX_ADDRESS_VERSION_BUFF_32: 6n, + 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":"Epoch33","clarity_version":"Clarity4", + 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>>, + crystallizeStakerRewards: {"name":"crystallize-staker-rewards","access":"private","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: 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":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[index: TypedAbiArg, isBond: 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>>, + updateAllowedCaller: {"name":"update-allowed-caller","access":"public","args":[{"name":"new-allowed-caller","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[newAllowedCaller: TypedAbiArg], Response>, + 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":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getStakerPendingRewardsForCycle: {"name":"get-staker-pending-rewards-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getStakerRewardsPerTokenPaidForCycle: {"name":"get-staker-rewards-per-token-paid-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: 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>, + stakerPendingRewardsForCycle: {"name":"staker-pending-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>, + stakerRewardsPaidPerTokenForCycle: {"name":"staker-rewards-paid-per-token-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, + allowedCaller: { + name: 'allowed-caller', + type: 'principal', + access: 'variable' +} as TypedAbiVariable + }, + constants: { + ERR_NO_CLAIMABLE_REWARDS: { + isOk: false, + value: 1_001n + }, + PRECISION: 1_000_000_000_000_000_000n, + allowedCaller: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM' +}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch33","clarity_version":"Clarity4", + 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>, + 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/stacking/common.ts b/stacking/common.ts index fb9a949..952b2e1 100644 --- a/stacking/common.ts +++ b/stacking/common.ts @@ -1,21 +1,9 @@ import { StackingClient } from '@stacks/stacking'; -import { StacksTestnet } from '@stacks/network'; -import { - getAddressFromPrivateKey, - TransactionVersion, - createStacksPrivateKey, -} from '@stacks/transactions'; +import { STACKS_TESTNET } from '@stacks/network'; +import { ClarityType, deserializeCV, getAddressFromPrivateKey } from '@stacks/transactions'; import { getPublicKeyFromPrivate, publicKeyToBtcAddress } from '@stacks/encryption'; -import { - InfoApi, - Configuration, - BlocksApi, - TransactionsApi, - SmartContractsApi, - AccountsApi, -} from '@stacks/blockchain-api-client'; -import pino, { Logger } from 'pino'; -import { ChainID } from '@stacks/common'; +import { createClient } from '@stacks/blockchain-api-client'; +import { Logger, pino } from 'pino'; const serviceName = process.env.SERVICE_NAME || 'JS'; export let logger: Logger; @@ -38,30 +26,28 @@ if (process.env.STACKS_LOG_JSON === '1') { }); } -export const CHAIN_ID = parseEnvInt('STACKS_CHAIN_ID', false) ?? ChainID.Testnet; +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 = new StacksTestnet({ url: nodeUrl }); +export const network = STACKS_TESTNET; network.chainId = CHAIN_ID; -const apiConfig = new Configuration({ - basePath: nodeUrl, +network.client.baseUrl = nodeUrl; +export const apiClient = createClient({ + baseUrl: nodeUrl, }); -export const infoApi = new InfoApi(apiConfig); -export const blocksApi = new BlocksApi(apiConfig); -export const txApi = new TransactionsApi(apiConfig); -export const contractsApi = new SmartContractsApi(apiConfig); -export const accountsApi = new AccountsApi(apiConfig); 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, TransactionVersion.Testnet); - const signerPrivKey = createStacksPrivateKey(privKey); - const signerPubKey = getPublicKeyFromPrivate(signerPrivKey.data); + const stxAddress = getAddressFromPrivateKey(privKey, network); + const signerPrivKey = privKey; + const signerPubKey = getPublicKeyFromPrivate(signerPrivKey); return { privKey, pubKey, @@ -71,14 +57,41 @@ export const accounts = process.env.STACKING_KEYS!.split(',').map((privKey, inde signerPubKey: signerPubKey, targetSlots: index + 1, index, - client: new StackingClient(stxAddress, network), + 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; @@ -86,9 +99,14 @@ export const maxAmount = MAX_U128; export async function waitForSetup() { try { - await accounts[0].client.getPoxInfo(); + await accounts[0]!.client.getPoxInfo(); } catch (error) { - if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) { + 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)); @@ -127,3 +145,38 @@ export function isPreparePhase(burnBlock: number) { 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/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar new file mode 100644 index 0000000..8d4281d --- /dev/null +++ b/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) 0x00) +(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/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar new file mode 100644 index 0000000..dcbb947 --- /dev/null +++ b/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/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar new file mode 100644 index 0000000..22aaa77 --- /dev/null +++ b/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/stacking/contracts/pox-5-signer.clar b/stacking/contracts/pox-5-signer.clar new file mode 100644 index 0000000..690e73a --- /dev/null +++ b/stacking/contracts/pox-5-signer.clar @@ -0,0 +1,296 @@ +(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 + +;; default to allowing deployer to register as a pool +(define-data-var allowed-caller principal tx-sender) + +(define-map rewards-per-token-for-cycle + { + index: uint, + is-bond: bool, + } + uint +) + +(define-map staker-rewards-paid-per-token-for-cycle + { + is-bond: bool, + index: uint, + staker: principal, + } + uint +) + +;; Represents pending, but unclaimed rewards for a staker +(define-map staker-pending-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 (update-allowed-caller (new-allowed-caller principal)) + (ok (var-set allowed-caller new-allowed-caller)) +) + +(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)) + ) + (crystallize-staker-rewards staker index (get is-bond acc)) + (ok acc) + ) +) + +(define-private (crystallize-staker-rewards + (staker principal) + (index uint) + (is-bond bool) + ) + (let ( + (earned (get-earned-staker-rewards staker index is-bond)) + (rewards-per-token (get-rewards-per-token-for-cycle index is-bond)) + ) + (map-set staker-pending-rewards-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + } + earned + ) + (map-set staker-rewards-paid-per-token-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) + (index uint) + (is-bond bool) + ) + (let ( + (shares (contract-call? .pox-5 get-staker-shares-staked-for-cycle staker + index is-bond current-contract + )) + (rpt-current (get-rewards-per-token-for-cycle index is-bond)) + (rpt-paid (get-staker-rewards-per-token-paid-for-cycle staker index is-bond)) + (pending (get-staker-pending-rewards-for-cycle staker index is-bond)) + (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) + ) + (+ pending newly-earned) + ) +) + +(define-public (claim-staker-rewards + (index uint) + (is-bond bool) + ) + (let ( + (staker tx-sender) + (rewards-info (crystallize-staker-rewards staker index is-bond)) + (earned (get earned rewards-info)) + ) + (asserts! (> earned u0) ERR_NO_CLAIMABLE_REWARDS) + (map-set staker-pending-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 + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? rewards-per-token-for-cycle { + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-staker-rewards-per-token-paid-for-cycle + (staker principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? staker-rewards-paid-per-token-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-staker-pending-rewards-for-cycle + (staker principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? staker-pending-rewards-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + }) + ) +) diff --git a/stacking/contracts/pox-5.clar b/stacking/contracts/pox-5.clar new file mode 100644 index 0000000..fb35809 --- /dev/null +++ b/stacking/contracts/pox-5.clar @@ -0,0 +1,2940 @@ +(define-constant ERR_UNAUTHORIZED (err u1)) +(define-constant ERR_CANNOT_SETUP_BOND_TOO_SOON (err u2)) +(define-constant ERR_CANNOT_SETUP_BOND_TOO_LATE (err u3)) +(define-constant ERR_BOND_ALREADY_SETUP (err u4)) +(define-constant ERR_STAKER_ALREADY_ADDED (err u5)) +(define-constant ERR_L1_LOCKUP_NOT_FOUND (err u6)) +(define-constant ERR_BOND_NOT_FOUND (err u7)) +(define-constant ERR_INSUFFICIENT_STX (err u8)) +(define-constant ERR_ALREADY_REGISTERED (err u9)) +(define-constant ERR_TOO_MUCH_SATS (err u10)) +(define-constant ERR_NOT_ALLOWLISTED (err u11)) +(define-constant ERR_SIGNER_KEY_GRANT_USED (err u12)) +(define-constant ERR_INVALID_SIGNATURE_RECOVER (err u13)) +(define-constant ERR_INVALID_SIGNATURE_PUBKEY (err u14)) +(define-constant ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH (err u15)) +(define-constant ERR_SIGNER_AUTH_USED (err u16)) +(define-constant ERR_SIGNER_KEY_GRANT_NOT_FOUND (err u17)) +(define-constant ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH (err u18)) +(define-constant ERR_ALREADY_STAKED (err u19)) +(define-constant ERR_INVALID_NUM_CYCLES (err u20)) +(define-constant ERR_INVALID_POX_ADDRESS (err u21)) +(define-constant ERR_UNAUTHORIZED_CALLER (err u22)) +(define-constant ERR_SIGNER_NOT_FOUND (err u23)) +(define-constant ERR_INVALID_START_BURN_HEIGHT (err u24)) +(define-constant ERR_NO_SBTC_BALANCE (err u25)) +(define-constant ERR_UNAUTHORIZED_SIGNER_REGISTRATION (err u26)) +(define-constant ERR_NOT_STAKING (err u27)) +(define-constant ERR_UNSTAKE_IN_PREPARE_PHASE (err u28)) +;; Trying to pay out to bonds in an invalid order +(define-constant ERR_INVALID_BOND_PERIOD_ORDERING (err u29)) +;; We already calculated at the start of this cycle +(define-constant ERR_DISTRIBUTION_ALREADY_COMPUTED (err u30)) +(define-constant ERR_BOND_NOT_ACTIVE (err u31)) +(define-constant ERR_NO_CLAIMABLE_REWARDS (err u32)) +(define-constant ERR_ACTIVE_BOND_NOT_INCLUDED (err u33)) +;; Not actively in a bond +(define-constant ERR_NOT_BOND_PARTICIPANT (err u34)) +;; A call to announce an early unlock was made +;; for a bond membership that has an L2 lockup +(define-constant ERR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK (err u35)) +;; The argument provided does not match the staker's signer +(define-constant ERR_INVALID_OLD_SIGNER_MANAGER (err u36)) +;; The amount of sats provided to unstake is invalid +(define-constant ERR_INVALID_UNSTAKE_SBTC_AMOUNT (err u37)) +;; The bond participant did not stake sBTC +(define-constant ERR_CANNOT_UNSTAKE_SBTC (err u38)) +;; A parse error occurred when reading a Bitcoin header +(define-constant ERR_READ_TX_OUT_OF_BOUNDS (err u39)) +;; An incorrect Bitcoin header was provided as part of a lockup proof +(define-constant ERR_INVALID_BTC_HEADER (err u40)) +;; An incorrect merkle proof was provided as part of a lockup proof +(define-constant ERR_INVALID_MERKLE_PROOF (err u41)) +;; The output script provided is incorrect +(define-constant ERR_INVALID_LOCKUP_SCRIPT (err u42)) +;; A staker tried to register for a bond after it already started +(define-constant ERR_BOND_ALREADY_STARTED (err u43)) +;; Cannot call `update-bond-registration` with the same signer +(define-constant ERR_UPDATE_BOND_SAME_SIGNER (err u44)) + +;; The length, in terms of staking cycles, of a given +;; bond period +(define-constant BOND_LENGTH_CYCLES u12) +;; The gap between the start of different bond periods +(define-constant BOND_GAP_CYCLES u2) +;; The maximum amount of time that a user can stake for +(define-constant MAX_NUM_CYCLES u96) + +;; The minimum amount of uSTX that a staker must stake +;; to become part of the signer set +(define-constant SIGNER_SET_MIN_USTX u50000000000) ;; 50k STX + +;; SIP18 message prefix +(define-constant SIP018_MSG_PREFIX 0x534950303138) + +;; SIP018 domain +(define-constant POX_5_SIGNER_DOMAIN { + name: "pox-5-signer", + version: "1.0.0", + chain-id: chain-id, +}) + +;; Keep these constants in lock-step with the address version buffs above +;; 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) + +;; Values for stacks address versions +(define-constant STACKS_ADDR_VERSION_MAINNET 0x16) +(define-constant STACKS_ADDR_VERSION_TESTNET 0x1a) + +;; Used to prevent fractional multiplication errors +;; during reward calculations +(define-constant PRECISION u1000000000000000000) ;; 1e18 + +;; The % of rewards that go to reserve, expressed +;; in basis points +(define-constant RESERVE_RATIO u1500) + +;; Core properties of protocol bonds +(define-map protocol-bonds + uint + { + ;; target yield rate (apy) in basis points + target-rate: uint, + ;; representation of STX:BTC price + ;; this value is equal to "ustx per 100 sats", which + ;; also translates to `(BTCUSD / STXUSD)`. + ;; used to determine bond priority + stx-value-ratio: uint, + ;; minimum amount of STX that must be locked + ;; relative to BTC for this term. + ;; Represented in basis points. + min-ustx-ratio: uint, + ;; The allowed early unlock signers for this bond period + early-unlock-signers: (buff 683), + ;; The Stacks principal that can announce early L1 unlocks + early-unlock-admin: principal, + } +) + +(define-map protocol-bond-allowances + { + bond-index: uint, + staker: principal, + } + ;; max amount of sats they can contribute + uint +) + +(define-map protocol-bond-memberships + principal + { + bond-index: uint, + amount-ustx: uint, + signer: principal, + is-l1-lock: bool, + } +) + +;; Total amount of sats staked per bond period +(define-map protocol-bonds-total-staked + uint + uint +) + +(define-map signer-key-grants + { + signer-key: (buff 33), + signer-manager: principal, + } + bool +) + +(define-map used-signer-key-grants + { + signer-key: (buff 33), + signer-manager: principal, + auth-id: uint, + } + bool +) + +;; Users can stake to a signer, where the signer owner +;; (which is the key of this map) is able to manage +;; the signer key for the signer. +(define-map signers + principal + (buff 33) ;; signer key +) + +;; Keep track of how much total STX has been delegated for a signer +;; for a given cycle. This includes from both protocol bonds and STX-only +;; stakers. This is the value that should be used to determine signer weight +;; when approving blocks. +(define-map signer-delegated-per-cycle + { + signer: principal, + cycle: uint, + } + uint +) + +;; Keep track of much much total STX has been staked, only through +;; STX-only signing, for this cycle. This may differ from +;; `signer-shares-staked-for-cycle`, which will be 0 if the total +;; amount delegated to this signer is below `SIGNER_SET_MIN_USTX`. +;; +;; Do not use for reward calculations! +(define-map signer-pending-staked-ustx-per-cycle + { + signer: principal, + cycle: uint, + } + uint +) + +;; Keep track of a staker's high-level info +(define-map staker-info + principal + { + amount-ustx: uint, + first-reward-cycle: uint, + num-cycles: uint, + signer: principal, + } +) + +;; Per-cycle staker signer membership. Only used for stx-only staking. +(define-map staker-signer-cycle-memberships + { + staker: principal, + cycle: uint, + } + { + amount-ustx: uint, + signer: principal, + } +) + +;; This represents the total uSTX delegated (through both +;; protocol bonds and STX-only staking) for a cycle +(define-map ustx-delegated-per-cycle + uint + uint +) + +;; allowed contract-callers +(define-map allowance-contract-callers + { + sender: principal, + contract-caller: principal, + } + ;; Optional expiration burn height + (optional uint) +) + +;; State for tracking used signer key authorizations. This prevents re-use +;; of the same signature or pre-set authorization for multiple transactions. +;; Refer to the `signer-key-authorizations` map for the documentation on these fields +(define-map used-signer-key-authorizations + { + signer-key: (buff 33), + reward-cycle: uint, + period: uint, + topic: (string-ascii 14), + pox-addr: (optional { + version: (buff 1), + hashbytes: (buff 32), + }), + auth-id: uint, + max-amount: uint, + } + bool ;; Whether the field has been used or not +) + +;; State to track the per-share rewards earned for bond periods +;; and reward cycles. This value must only increment +(define-map rewards-per-token-for-cycle + { + is-bond: bool, + index: uint, + } + uint +) + +;; Total shares (either ustx or sats) staked in a given +;; bond or stx-only cycle +(define-map total-shares-staked-for-cycle + { + index: uint, + is-bond: bool, + } + uint +) + +;; State to track the per-staker shares for a given signer. +(define-map staker-shares-staked-for-cycle + { + index: uint, + is-bond: bool, + staker: principal, + signer: principal, + } + uint +) + +;; Amount of shares staked for a given signer in a given cycle. +;; This is strictly for reward calculations - ie the STX +;; from Bitcoin staking are not accounted for here. +(define-map signer-shares-staked-for-cycle + { + index: uint, + is-bond: bool, + signer: principal, + } + uint +) + +;; Represents a snapshot of `rewards-per-token` at the last +;; time of rewards calculation for this specific signer +(define-map signer-rewards-per-token-paid-for-cycle + { + is-bond: bool, + index: uint, + signer: principal, + } + uint +) + +;; Represents pending, but unclaimed rewards for a signer +(define-map signer-pending-rewards-for-cycle + { + is-bond: bool, + index: uint, + signer: principal, + } + uint +) + +;; The role that is allowed to set bond parameters. +;; On non-mainnet networks `make_pox_5_body` rewrites the literal to the +;; configured admin before deploy. +;; TODO: this should be set to some predefined multisig for mainnet. +(define-data-var bond-admin principal 'SP000000000000000000002Q6VF78) + +;; Data vars that store a copy of the burnchain configuration. +;; Implemented as data-vars, so that different configurations can be +;; used in e.g. test harnesses. +;; #[allow(unused_data_var)] +(define-data-var pox-prepare-cycle-length uint (if is-in-mainnet + u100 + u50 +)) +(define-data-var pox-reward-cycle-length uint (if is-in-mainnet + u2100 + u1050 +)) +(define-data-var first-burnchain-block-height uint u0) +(define-data-var configured bool false) +;; The first reward cycle where pox-5 is active. This +;; is also equal to the first bond period. +(define-data-var first-pox-5-reward-cycle uint u0) +;; The first reward cycle where the first bond period occurs +(define-data-var first-bond-period-cycle uint u0) + +;;;; The last accounted balance (of sBTC held by this contract) +;;;; at a time of reward computation. +;;;; N.B. it is critical that this value is set to the contract's +;;;; sBTC balance after any transfer of sBTC out of this contract. +;; (define-data-var last-accounted-balance uint u0) + +;; The last accounted balance of rewards. Used to keep +;; track of which sBTC is just for rewards, vs from +;; staking. +(define-data-var last-accounted-rewards-only uint u0) + +;; The last burn height in which rewards were calculated +(define-data-var last-reward-compute-height uint u0) + +;; the amount of sBTC claimable by the reserve +(define-data-var reserve-balance uint u0) + +;; The total amount of sBTC staked +(define-data-var total-sbtc-staked uint u0) + +(define-trait signer-manager-trait ( + (validate-stake! + ;; staker, first-index, num-indexes, amount-ustx, amount-sats, is-bond, signer-calldata + (principal uint uint uint uint bool (optional (buff 500))) + (response bool uint) + ) + (checkpoint-staker + ;; staker, first-index, num-indexes, is-bond + (principal uint uint bool) + (response bool uint) + ) +)) + +;; This function can only be called once, when it boots up +(define-public (set-burnchain-parameters + (first-burn-height uint) + (prepare-cycle-length uint) + (reward-cycle-length uint) + (begin-pox5-reward-cycle uint) + ) + (begin + (unwrap-panic (if (var-get configured) + (err false) + (ok true) + )) + (var-set first-burnchain-block-height first-burn-height) + (var-set pox-prepare-cycle-length prepare-cycle-length) + (var-set pox-reward-cycle-length reward-cycle-length) + (var-set first-pox-5-reward-cycle begin-pox5-reward-cycle) + (var-set first-bond-period-cycle begin-pox5-reward-cycle) + (var-set configured true) + (ok true) + ) +) + +(define-public (set-bond-admin (new-admin principal)) + (begin + ;; only bond admin can call this. + (asserts! (is-eq contract-caller (var-get bond-admin)) ERR_UNAUTHORIZED) + (ok (var-set bond-admin new-admin)) + ) +) + +;; Setup a new protocol bond by providing parameters and the +;; allowlist for the bond. +;; +;; @param target-rate; target yield rate (apy) in basis points +;; @param stx-value-ratio; representation of STX:BTC price +;; @param min-ustx-ratio; minimum amount of STX that must be locked +;; relative to BTC for this term. Represented in basis points. +;; @param early-exit-signers: An allowlist of bond members that can +;; participate in the bond. +;; +;; This function can only be called once. +(define-public (setup-bond + (bond-index uint) + (target-rate uint) + (stx-value-ratio uint) + (min-ustx-ratio uint) + (early-unlock-signers (buff 683)) + (early-unlock-admin principal) + (allowlist (list 1000 { + staker: principal, + max-sats: uint, + })) + ) + (let ((bond-start-height (bond-period-to-burn-height bond-index))) + ;; only bond admin can call this. + (asserts! (is-eq contract-caller (var-get bond-admin)) ERR_UNAUTHORIZED) + + ;; only can be called within 2 cycles of bond start + (asserts! + (or + ;; prevent underflow + (< bond-start-height + (* BOND_GAP_CYCLES (var-get pox-reward-cycle-length)) + ) + (<= + (- bond-start-height + (* BOND_GAP_CYCLES (var-get pox-reward-cycle-length)) + ) + burn-block-height + ) + ) + ERR_CANNOT_SETUP_BOND_TOO_SOON + ) + + ;; only can be called before bond start + (asserts! (< burn-block-height bond-start-height) + ERR_CANNOT_SETUP_BOND_TOO_LATE + ) + + (asserts! + (map-insert protocol-bonds bond-index { + target-rate: target-rate, + stx-value-ratio: stx-value-ratio, + min-ustx-ratio: min-ustx-ratio, + early-unlock-signers: early-unlock-signers, + early-unlock-admin: early-unlock-admin, + }) + ERR_BOND_ALREADY_SETUP + ) + + (let ((accumulator (try! (fold add-staker-to-bond allowlist + (ok { + sum-max-sats: u0, + bond-index: bond-index, + }) + )))) + (ok { + bond-index: bond-index, + target-rate: target-rate, + stx-value-ratio: stx-value-ratio, + min-ustx-ratio: min-ustx-ratio, + early-unlock-signers: early-unlock-signers, + max-allocation-sats: (get sum-max-sats accumulator), + }) + ) + ) +) + +(define-private (add-staker-to-bond + (staker-item { + staker: principal, + max-sats: uint, + }) + (accumulator-res (response { + sum-max-sats: uint, + bond-index: uint, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (bond-index (get bond-index accumulator)) + ) + (asserts! + (map-insert protocol-bond-allowances { + bond-index: bond-index, + staker: (get staker staker-item), + } + (get max-sats staker-item) + ) + ERR_STAKER_ALREADY_ADDED + ) + (print (merge staker-item { + topic: "add-to-allowlist", + bond-index: bond-index, + })) + (ok { + sum-max-sats: (+ (get sum-max-sats accumulator) (get max-sats staker-item)), + bond-index: bond-index, + }) + ) +) + +;; Register for a protocol bond. In order the call this function, +;; the bond must already have been created, and `contract-caller` +;; must be in the allowlist. +;; +;; The caller must either provide sBTC that they want to lockup, +;; or they must provide proof of their L1 BTC lockup. +(define-public (register-for-bond + (bond-index uint) + (signer-manager ) + (amount-ustx uint) + ;; Their BTC lockup info. If the response is `ok`, then + ;; this is a list of outputs corresponding to their timelocks. + ;; If the response is `err`, this is the amount of sBTC (in sats) + ;; that they want to lock. + (btc-lockup (response { + outputs: (list 10 + { + height: uint, + tx: (buff 100000), + output-index: uint, + header: (buff 80), + leaf-hashes: (list 14 (buff 32)), + tx-count: uint, + tx-index: uint, + amount: uint, + } + ), + unlock-bytes: (buff 683), + } + uint + )) + (signer-calldata (optional (buff 500))) + ) + (let ( + (signer (contract-of signer-manager)) + (sats-total (try! (match btc-lockup + l1-lockups (verify-l1-lockups tx-sender bond-index l1-lockups) + sbtc-amount (lock-sbtc sbtc-amount) + ))) + (bond (unwrap! (map-get? protocol-bonds bond-index) ERR_BOND_NOT_FOUND)) + (allowance (unwrap! + (map-get? protocol-bond-allowances { + staker: tx-sender, + bond-index: bond-index, + }) + ERR_NOT_ALLOWLISTED + )) + (first-reward-cycle (bond-period-to-reward-cycle bond-index)) + (bond-start-height (bond-period-to-burn-height bond-index)) + ;; the first cycle in which their stx are unlocked + (unlock-cycle (+ first-reward-cycle BOND_LENGTH_CYCLES)) + (current-total-staked (get-total-shares-staked-for-cycle bond-index true)) + (current-signer-staked (get-signer-shares-staked-for-cycle signer bond-index true)) + ) + ;; Verify that they're sending enough STX + (asserts! + (>= amount-ustx + (min-ustx-for-sats-amount sats-total (get stx-value-ratio bond) + (get min-ustx-ratio bond) + )) + ERR_INSUFFICIENT_STX + ) + + ;; Verify that the bond hasn't started + (asserts! (< burn-block-height bond-start-height) + ERR_BOND_ALREADY_STARTED + ) + + ;; Cannot be already staked + (asserts! (is-none (get-staker-info tx-sender)) ERR_ALREADY_STAKED) + + (asserts! (<= sats-total allowance) ERR_TOO_MUCH_SATS) + + ;; Validate that the staker can join this signer + (try! (contract-call? signer-manager validate-stake! tx-sender bond-index u1 + amount-ustx sats-total true signer-calldata + )) + ;; The signer must have been registered already + (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;;;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + (asserts! (is-none (get-bond-membership tx-sender)) + ERR_ALREADY_REGISTERED + ) + + (map-set protocol-bond-memberships tx-sender { + bond-index: bond-index, + amount-ustx: amount-ustx, + signer: signer, + is-l1-lock: (is-ok btc-lockup), + }) + (map-set protocol-bonds-total-staked bond-index + (+ current-total-staked sats-total) + ) + (crystallize-rewards signer bond-index true) + (map-set total-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + } + (+ current-total-staked sats-total) + ) + (map-set signer-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + signer: signer, + } + (+ current-signer-staked sats-total) + ) + (map-set staker-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + staker: tx-sender, + signer: signer, + } + sats-total + ) + + (try! (add-staker-to-signer-cycles tx-sender signer first-reward-cycle + BOND_LENGTH_CYCLES amount-ustx false + )) + + (ok { + signer: signer, + staker: tx-sender, + amount-ustx: amount-ustx, + bond-index: bond-index, + first-reward-cycle: first-reward-cycle, + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + unlock-cycle: unlock-cycle, + }) + ) +) + +;; As a bond participant, update your signer. This takes effect +;; in the next reward cycle where this bond participant is active. +;; +;; Note that if the bond hasn't started yet, it's possible for the staker +;; to not be active in the next reward cycle. In that case, the signer is updated +;; from the start of the bond period. +(define-public (update-bond-registration + (signer-manager ) + (old-signer-manager ) + (signer-calldata (optional (buff 500))) + ) + (let ( + (signer (contract-of signer-manager)) + (old-signer (contract-of old-signer-manager)) + (current-membership (unwrap! (get-bond-membership tx-sender) ERR_NOT_BOND_PARTICIPANT)) + (current-signer (get signer current-membership)) + (bond-index (get bond-index current-membership)) + (amount-sats (get-staker-shares-staked-for-cycle tx-sender bond-index true + current-signer + )) + (bond-start-cycle (bond-period-to-reward-cycle bond-index)) + (bond-end-cycle (bond-period-to-reward-cycle (+ bond-index u6))) + (next-cycle (+ (current-pox-reward-cycle) u1)) + (current-signer-total-sats (get-signer-shares-staked-for-cycle current-signer bond-index true)) + (new-signer-total-sats (get-signer-shares-staked-for-cycle signer bond-index true)) + ;; If the bond hasn't started yet, then the first cycle where + ;; this new signer is active is the start cycle. Otherwise, it's the next reward + ;; cycle. In other words, `max(bond-start-cycle, current-cycle + 1)` + (first-reward-cycle (if (> bond-start-cycle next-cycle) + bond-start-cycle + next-cycle + )) + (num-cycles (- bond-end-cycle first-reward-cycle)) + ) + (asserts! (is-eq old-signer current-signer) + ERR_INVALID_OLD_SIGNER_MANAGER + ) + + ;; Validate that the new signer is different + (asserts! (not (is-eq signer old-signer)) ERR_UPDATE_BOND_SAME_SIGNER) + + ;; Validate that the staker can join this signer + (try! (contract-call? signer-manager validate-stake! tx-sender bond-index u1 + (get amount-ustx current-membership) amount-sats true + signer-calldata + )) + + ;; Call `old-signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? old-signer-manager checkpoint-staker tx-sender bond-index + u1 true + ) + ok-val ok-val + err-val true + ) + + ;; The signer must have been registered already + (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + (crystallize-rewards current-signer bond-index true) + (crystallize-rewards signer bond-index true) + + ;; Remove the staker from all existing cycles + (try! (remove-staker-from-cycles tx-sender first-reward-cycle num-cycles false)) + + ;; Re-add to existing cycles with the new signer + (try! (add-staker-to-signer-cycles tx-sender signer first-reward-cycle + num-cycles (get amount-ustx current-membership) false + )) + + ;; Remove the sBTC shares from the current signer + (map-delete staker-shares-staked-for-cycle { + index: bond-index, + staker: tx-sender, + signer: current-signer, + is-bond: true, + }) + (map-set signer-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + signer: current-signer, + } + (- current-signer-total-sats amount-sats) + ) + + ;; Add the sBTC shares to the current signer + (map-set staker-shares-staked-for-cycle { + index: bond-index, + staker: tx-sender, + signer: signer, + is-bond: true, + } + amount-sats + ) + (map-set signer-shares-staked-for-cycle { + index: bond-index, + signer: signer, + is-bond: true, + } + (+ new-signer-total-sats amount-sats) + ) + (map-set protocol-bond-memberships tx-sender { + bond-index: bond-index, + amount-ustx: (get amount-ustx current-membership), + signer: signer, + is-l1-lock: (get is-l1-lock current-membership), + }) + + (ok true) + ) +) + +;; Register a signer +(define-public (register-signer + (signer-manager ) + (signer-key (buff 33)) + ) + (let ((signer (contract-of signer-manager))) + ;; Because signers can have members register at any time, + ;; they must use signer key grants instead of per-tx + ;; authorizations. + (try! (verify-signer-key-grant signer signer-key)) + + ;; Only the signer contract itself can register itself + (asserts! (is-eq tx-sender signer) ERR_UNAUTHORIZED_SIGNER_REGISTRATION) + + (map-set signers signer signer-key) + (ok { + signer: signer, + signer-key: signer-key, + }) + ) +) + +;; Stake your STX +(define-public (stake + (signer-manager ) + (amount-ustx uint) + (num-cycles uint) + (start-burn-ht uint) + (signer-calldata (optional (buff 500))) + ) + (let ( + (signer (contract-of signer-manager)) + (current-cycle (current-pox-reward-cycle)) + (first-reward-cycle (+ u1 current-cycle)) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) + ;; the first cycle in which their stx are unlocked + (unlock-cycle (+ first-reward-cycle num-cycles)) + ) + ;; Validate that the staker can join this signer + (try! (contract-call? signer-manager validate-stake! tx-sender + first-reward-cycle num-cycles amount-ustx u0 false + signer-calldata + )) + ;; The signer must have been registered already + (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + ERR_INVALID_START_BURN_HEIGHT + ) + + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) + + ;;;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + ;; Cannot be already staked + (asserts! (is-none (get-staker-info tx-sender)) ERR_ALREADY_STAKED) + + ;;;; tx-sender principal must not be in a bond membership + (asserts! (is-none (get-bond-membership tx-sender)) ERR_ALREADY_STAKED) + + ;;;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance tx-sender) amount-ustx) + ERR_INSUFFICIENT_STX + ) + + (try! (add-staker-to-signer-cycles tx-sender signer first-reward-cycle + num-cycles amount-ustx true + )) + + (map-set staker-info tx-sender { + amount-ustx: amount-ustx, + first-reward-cycle: first-reward-cycle, + num-cycles: num-cycles, + signer: signer, + }) + + (ok { + signer: signer, + staker: tx-sender, + amount-ustx: amount-ustx, + num-cycle: num-cycles, + first-reward-cycle: first-reward-cycle, + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + unlock-cycle: unlock-cycle, + }) + ) +) + +;; A user can: +;; - Change signers +;; - Extend their lock +;; - Increase STX locked +(define-public (stake-update + (signer-manager ) + (old-signer-manager ) + (cycles-to-extend uint) + (amount-increase uint) + (signer-calldata (optional (buff 500))) + ) + (let ( + (signer (contract-of signer-manager)) + (old-signer (contract-of old-signer-manager)) + (current-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKING)) + ;; This is the first cycle where their STX would be unlocked + (prev-unlock-cycle (+ (get first-reward-cycle current-info) + (get num-cycles current-info) + )) + (unlock-cycle (+ prev-unlock-cycle cycles-to-extend)) + (new-lock-amount (+ (get amount-ustx current-info) amount-increase)) + (current-cycle (current-pox-reward-cycle)) + (first-reward-cycle (+ current-cycle u1)) + (num-cycles (- unlock-cycle current-cycle u1)) + ) + ;; Validate that the staker can join this signer + (try! (contract-call? signer-manager validate-stake! tx-sender + first-reward-cycle num-cycles new-lock-amount u0 false + signer-calldata + )) + ;; Validate that `old-signer-manager` matches their current signer + (asserts! (is-eq old-signer (get signer current-info)) + ERR_INVALID_OLD_SIGNER_MANAGER + ) + ;; The signer must have been registered already + (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) + + ;;;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + ;; Must have enough unlocked STX + (asserts! (>= (get unlocked (stx-account tx-sender)) amount-increase) + ERR_INSUFFICIENT_STX + ) + + ;; Call `old-signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? old-signer-manager checkpoint-staker tx-sender + first-reward-cycle (- prev-unlock-cycle current-cycle u1) false + ) + ;; Allow any errors + ok-val + ok-val + err-val + true + ) + + ;; Remove the staker from all existing cycles + (try! (remove-staker-from-cycles tx-sender (+ u1 current-cycle) + (- prev-unlock-cycle current-cycle u1) true + )) + + (try! (add-staker-to-signer-cycles tx-sender signer (+ u1 current-cycle) + num-cycles new-lock-amount true + )) + + (map-set staker-info tx-sender { + amount-ustx: new-lock-amount, + first-reward-cycle: (get first-reward-cycle current-info), + num-cycles: (+ (get num-cycles current-info) cycles-to-extend), + signer: signer, + }) + + (ok { + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + staker: tx-sender, + signer: signer, + prev-unlock-height: prev-unlock-cycle, + unlock-cycle: unlock-cycle, + num-cycles: num-cycles, + amount-ustx: new-lock-amount, + }) + ) +) + +(define-public (announce-l1-early-exit + (staker principal) + (old-signer-manager ) + ) + (let ( + (old-signer (contract-of old-signer-manager)) + (membership (unwrap! (get-bond-membership staker) ERR_NOT_BOND_PARTICIPANT)) + (bond-index (get bond-index membership)) + (signer (get signer membership)) + (bond (unwrap-panic (get-protocol-bond bond-index))) + (amount-sats (get-staker-shares-staked-for-cycle staker bond-index true signer)) + (current-total-shares (get-total-shares-staked-for-cycle bond-index true)) + (current-shares (get-signer-shares-staked-for-cycle signer bond-index true)) + ) + ;; Only the early unlock admin for this bond period can call this function. + ;; Calling via other contracts is not allowed. + (asserts! + (and (is-eq contract-caller tx-sender) (is-eq contract-caller (get early-unlock-admin bond))) + ERR_UNAUTHORIZED + ) + (asserts! (get is-l1-lock membership) ERR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK) + (asserts! (is-eq old-signer signer) ERR_INVALID_OLD_SIGNER_MANAGER) + + ;; Call `old-signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? old-signer-manager checkpoint-staker staker bond-index u1 + true + ) + ok-val ok-val + err-val true + ) + + (crystallize-rewards signer bond-index true) + + (map-set staker-shares-staked-for-cycle { + is-bond: true, + staker: staker, + signer: signer, + index: bond-index, + } + u0 + ) + (map-set signer-shares-staked-for-cycle { + is-bond: true, + signer: signer, + index: bond-index, + } + (- current-shares amount-sats) + ) + (map-set total-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + } + (- current-total-shares amount-sats) + ) + (ok true) + ) +) + +;; As a bond participant with locked sBTC, remove a portion (or all) +;; of your locked sBTC. +(define-public (unstake-sbtc + (signer-manager ) + (amount-to-withdrawal-sats uint) + ) + (let ( + (staker tx-sender) + (membership (unwrap! (map-get? protocol-bond-memberships staker) + ERR_NOT_BOND_PARTICIPANT + )) + (bond-index (get bond-index membership)) + (signer (get signer membership)) + (current-amount-sats (get-staker-shares-staked-for-cycle staker bond-index true signer)) + (current-total-shares (get-total-shares-staked-for-cycle bond-index true)) + (current-shares (get-signer-shares-staked-for-cycle signer bond-index true)) + (current-total-sbtc-staked (get-total-sbtc-staked)) + ;; Cannot withdrawal more than they've staked + (new-amount-sats (try! (if (<= amount-to-withdrawal-sats current-amount-sats) + + (ok (- current-amount-sats amount-to-withdrawal-sats)) + ERR_INVALID_UNSTAKE_SBTC_AMOUNT + ))) + ) + ;; `signer-manager` must match the current signer + (asserts! (is-eq (contract-of signer-manager) signer) + ERR_INVALID_OLD_SIGNER_MANAGER + ) + + ;; Must be an sBTC lock + (asserts! (not (get is-l1-lock membership)) ERR_CANNOT_UNSTAKE_SBTC) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + ;; Call `signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? signer-manager checkpoint-staker staker bond-index u1 + true + ) + ok-val ok-val + err-val true + ) + + ;; Take a snapshot of the signer's current rewards + (crystallize-rewards signer bond-index true) + + (map-set staker-shares-staked-for-cycle { + is-bond: true, + staker: staker, + signer: signer, + index: bond-index, + } + new-amount-sats + ) + (map-set signer-shares-staked-for-cycle { + is-bond: true, + signer: signer, + index: bond-index, + } + (- current-shares amount-to-withdrawal-sats) + ) + (map-set total-shares-staked-for-cycle { + is-bond: true, + index: bond-index, + } + (- current-total-shares amount-to-withdrawal-sats) + ) + (var-set total-sbtc-staked + (- current-total-sbtc-staked amount-to-withdrawal-sats) + ) + + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" amount-to-withdrawal-sats + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer amount-to-withdrawal-sats tx-sender staker none + )) + )) + + (ok { + staker: staker, + signer: signer, + new-amount-sats: new-amount-sats, + }) + ) +) + +;; Unstake - set your STX to unlock at the end of the current cycle +(define-public (unstake (old-signer-manager )) + (let ( + (old-signer (contract-of old-signer-manager)) + (current-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKING)) + (first-reward-cycle (get first-reward-cycle current-info)) + ;; This is the first cycle where their STX would be unlocked + (prev-unlock-cycle (+ first-reward-cycle (get num-cycles current-info))) + (current-cycle (current-pox-reward-cycle)) + (unlock-cycle (+ current-cycle u1)) + ) + (asserts! (is-eq old-signer (get signer current-info)) + ERR_INVALID_OLD_SIGNER_MANAGER + ) + ;;;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + ;; do not allow during a prepare phase + (asserts! (not (is-in-prepare-phase current-cycle)) + ERR_UNSTAKE_IN_PREPARE_PHASE + ) + + ;; Call `old-signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? old-signer-manager checkpoint-staker tx-sender + (+ current-cycle u1) (- prev-unlock-cycle current-cycle u1) + false + ) + ok-val ok-val + err-val true + ) + + ;; Remove the staker from all existing cycles + (try! (remove-staker-from-cycles tx-sender (+ u1 current-cycle) + (- prev-unlock-cycle current-cycle u1) true + )) + + (map-set staker-info tx-sender { + amount-ustx: (get amount-ustx current-info), + first-reward-cycle: first-reward-cycle, + num-cycles: (- unlock-cycle first-reward-cycle), + signer: old-signer, + }) + + (ok { + staker: tx-sender, + amount-ustx: (get amount-ustx current-info), + first-reward-cycle: first-reward-cycle, + unlock-cycle: unlock-cycle, + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + }) + ) +) + +;;;; Remove a staker from a signer for X cycles +(define-private (remove-staker-from-cycles + (staker principal) + (first-reward-cycle uint) + (num-cycles uint) + (is-stx-staking bool) + ) + (ok (try! (fold remove-staker-from-signer-for-cycle + ;; panic is ok here because we've already checked `num-cycles` + (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-cycles + )) + (ok { + staker: staker, + first-reward-cycle: first-reward-cycle, + is-stx-staking: is-stx-staking, + }) + ))) +) + +;; For a given (staker, signer, cycle), remove a staker from +;; that signer. If the signer has gone below the minimum amount to +;; be in the signer set, remove them from the signer set. +(define-private (remove-staker-from-signer-for-cycle + (cycle-index uint) + (accumulator-res (response { + staker: principal, + first-reward-cycle: uint, + is-stx-staking: bool, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (staker (get staker accumulator)) + (cycle (+ cycle-index (get first-reward-cycle accumulator))) + (membership (unwrap! + (map-get? staker-signer-cycle-memberships { + staker: staker, + cycle: cycle, + }) + ERR_NOT_STAKING + )) + (signer (get signer membership)) + ;; Get the total uSTX delegated (through protocol bonds and STX-only + ;; staking) to this signer. + (cur-delegated-for-signer (get-amount-delegated-for-signer signer cycle)) + ;; uSTX staked for this signer (through STX-only staking) + (cur-staked-for-signer (get-signer-shares-staked-for-cycle signer cycle false)) + ;; Total uSTX staked (through stx-only staking) this cycle + (total-shares-staked (get-total-shares-staked-for-cycle cycle false)) + (amount (get amount-ustx membership)) + (is-stx-staking (get is-stx-staking accumulator)) + (stake-amount (if is-stx-staking + amount + u0 + )) + (new-delegated (- cur-delegated-for-signer amount)) + (is-in-signer-set (is-some (get-signer-set-item-for-cycle signer cycle))) + ) + ;; Crystallize STX-only rewards before mutating anything + (crystallize-rewards signer cycle false) + (if is-in-signer-set + (if (< new-delegated SIGNER_SET_MIN_USTX) + ;; They've crossed back below the threshold - remove from the signer set + ;; and remove from reward calculations. + (begin + (try! (remove-staker-from-set-for-cycle signer cycle)) + (map-set signer-shares-staked-for-cycle { + index: cycle, + signer: signer, + is-bond: false, + } + u0 + ) + (map-set total-shares-staked-for-cycle { + index: cycle, + is-bond: false, + } + (- total-shares-staked cur-staked-for-signer) + ) + ) + ;; They are in the signer set - update reward calculations + (begin + (map-set total-shares-staked-for-cycle { + index: cycle, + is-bond: false, + } + (- total-shares-staked stake-amount) + ) + (map-set signer-shares-staked-for-cycle { + index: cycle, + is-bond: false, + signer: signer, + } + (- cur-staked-for-signer stake-amount) + ) + ) + ) + true + ) + ;; Remove this staker from this signer + (map-delete staker-signer-cycle-memberships { + staker: staker, + cycle: cycle, + }) + ;; Update amount delegated + (map-set signer-delegated-per-cycle { + cycle: cycle, + signer: signer, + } + new-delegated + ) + ;; Remove amount for staker + (map-delete staker-shares-staked-for-cycle { + index: cycle, + is-bond: false, + staker: staker, + signer: signer, + }) + ;; Update amount staked + (map-set signer-pending-staked-ustx-per-cycle { + signer: signer, + cycle: cycle, + } + (- (get-signer-pending-staked-ustx-per-cycle signer cycle) + stake-amount + )) + ;; Update total amount delegated this cycle + (map-set ustx-delegated-per-cycle cycle + (- (get-ustx-delegated-for-cycle cycle) amount) + ) + (ok accumulator) + ) +) + +(define-private (add-staker-to-signer-cycles + (staker principal) + (signer principal) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint) + (is-stx-staking bool) + ) + (ok (try! (fold add-staker-to-signer-for-cycle + ;; panic is ok here because we've already checked `num-cycles` + (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-cycles + )) + (ok { + staker: staker, + signer: signer, + amount-ustx: amount-ustx, + first-reward-cycle: first-reward-cycle, + is-stx-staking: is-stx-staking, + }) + ))) +) + +;; For a given (staker, signer, cycle), update signer state for that +;; cycle and lazily add the signer to the signer set if needed. +;; +;; We also update state for the total STX delegated to this signer, +;; along with the total of STX staked in STX-only staking for this signer. +;; +;; If the signer is above the minimum threshold, only then do we update +;; reward calculation state, so that signers below the _delegation_ threshold +;; don't receive rewards. This means it's possible for a signer to have +;; _more_ than the minimum delegated, but _less_ staked from STX-only stakers, +;; but they'll still receive rewards. +(define-private (add-staker-to-signer-for-cycle + (cycle-index uint) + (accumulator-res (response { + signer: principal, + staker: principal, + amount-ustx: uint, + first-reward-cycle: uint, + is-stx-staking: bool, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (cycle (+ cycle-index (get first-reward-cycle accumulator))) + (signer (get signer accumulator)) + ;; Get the total uSTX delegated (through protocol bonds and STX-only + ;; staking) to this signer. + (cur-delegated-for-signer (get-amount-delegated-for-signer signer cycle)) + (amount (get amount-ustx accumulator)) + (stake-amount (if (get is-stx-staking accumulator) + amount + u0 + )) + (staker (get staker accumulator)) + (prev-staked (get-signer-pending-staked-ustx-per-cycle signer cycle)) + (prev-total-shares-staked (get-total-shares-staked-for-cycle cycle false)) + (new-delegated (+ cur-delegated-for-signer amount)) + ) + ;; Crystallize STX-only rewards before mutating anything + (crystallize-rewards signer cycle false) + (if (>= new-delegated SIGNER_SET_MIN_USTX) + (begin + (map-set signer-shares-staked-for-cycle { + index: cycle, + is-bond: false, + signer: signer, + } + (+ prev-staked stake-amount) + ) + (if (< cur-delegated-for-signer SIGNER_SET_MIN_USTX) + ;; They just crossed the threshold - add to signer set and add to reward calculations + (begin + (try! (add-signer-to-set-for-cycle signer cycle)) + (map-set total-shares-staked-for-cycle { + index: cycle, + is-bond: false, + } + (+ prev-total-shares-staked prev-staked stake-amount) + ) + ) + ;; They're already over the threshold - update the total by just `stake-amount` + (map-set total-shares-staked-for-cycle { + index: cycle, + is-bond: false, + } + (+ prev-total-shares-staked stake-amount) + ) + ) + ) + + ;; not over the min yet + true + ) + ;; Add the staker's membership + (map-set staker-signer-cycle-memberships { + staker: staker, + cycle: cycle, + } { + signer: signer, + amount-ustx: amount, + }) + ;; Update the amount delegated + (map-set signer-delegated-per-cycle { + cycle: cycle, + signer: signer, + } + new-delegated + ) + ;; Update the amount staked for this signer + (map-set signer-pending-staked-ustx-per-cycle { + signer: signer, + cycle: cycle, + } + (+ prev-staked stake-amount) + ) + ;; Update the amount staked for this staker + (map-set staker-shares-staked-for-cycle { + staker: staker, + index: cycle, + is-bond: false, + signer: signer, + } + stake-amount + ) + ;; Set the total ustx delegated this cycle + (map-set ustx-delegated-per-cycle cycle + (+ (get-ustx-delegated-for-cycle cycle) amount) + ) + (ok accumulator) + ) +) + +(define-private (lock-sbtc (amount uint)) + (begin + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer amount tx-sender current-contract none + )) + (var-set total-sbtc-staked (+ (var-get total-sbtc-staked) amount)) + (ok amount) + ) +) + +;; Verify l1 lockup information for a staker. This asserts that each lockup +;; corresponds to the right timelock script for this staker, and that the lockup +;; occurred on-chain. If everything is valid, this returns the sum of all lockups in sats. +(define-private (verify-l1-lockups + (staker principal) + (bond-index uint) + (lockups { + outputs: (list 10 + { + height: uint, + tx: (buff 100000), + output-index: uint, + header: (buff 80), + leaf-hashes: (list 14 (buff 32)), + tx-count: uint, + tx-index: uint, + amount: uint, + } + ), + unlock-bytes: (buff 683), + }) + ) + (let ( + (bond (unwrap! (get-protocol-bond bond-index) ERR_BOND_NOT_FOUND)) + (expected-timelock-output (construct-lockup-output-script staker + (get-bond-l1-unlock-height bond-index) + (get unlock-bytes lockups) (get early-unlock-signers bond) + )) + (accumulation (try! (fold validate-l1-lockup (get outputs lockups) + (ok { + sum: u0, + expected-script-hash: expected-timelock-output, + }) + ))) + ) + (ok (get sum accumulation)) + ) +) + +;; Fold function for validating l1 lockup info +(define-private (validate-l1-lockup + (lockup { + height: uint, + tx: (buff 100000), + output-index: uint, + header: (buff 80), + leaf-hashes: (list 14 (buff 32)), + tx-count: uint, + tx-index: uint, + amount: uint, + }) + (accumulator-res (response { + expected-script-hash: (buff 34), + sum: uint, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (block (try! (parse-block-header (get header lockup)))) + (expected-script-hash (get expected-script-hash accumulator)) + (output (try! (get-bitcoin-tx-output? (get tx lockup) (get output-index lockup) + (get amount lockup) expected-script-hash + ))) + (reversed-txid (get txid output)) + (txid (reverse-buff32 reversed-txid)) + ) + (asserts! (verify-block-header (get header lockup) (get height lockup)) + ERR_INVALID_BTC_HEADER + ) + (asserts! (is-eq (get script output) expected-script-hash) + ERR_INVALID_LOCKUP_SCRIPT + ) + ;; verify merkle proof + (asserts! + (or + (is-eq (get merkle-root block) txid) ;; true, if the transaction is the only transaction + (verify-merkle-proof reversed-txid + (reverse-buff32 (get merkle-root block)) + (get tx-index lockup) (get tx-count lockup) + (get leaf-hashes lockup) + ) + ) + ERR_INVALID_MERKLE_PROOF + ) + (ok { + expected-script-hash: (get expected-script-hash accumulator), + sum: (+ (get sum accumulator) (get amount output)), + }) + ) +) + +;;; Reward calculation + +;; Returns the total balance of rewards received by the contract +(define-read-only (get-rewards) + (let ( + (cur-reserve (var-get reserve-balance)) + (total-staked-sbtc (get-total-sbtc-staked)) + (current-balance (unwrap-panic (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + get-balance current-contract + ))) + ) + (- current-balance total-staked-sbtc cur-reserve) + ) +) + +;; Returns the total amount of newly received sBTC rewards +;; since the last rewards computation +(define-read-only (get-new-rewards) + (let ( + (last-accounted-rewards (var-get last-accounted-rewards-only)) + (rewards-balance (get-rewards)) + ) + (- rewards-balance last-accounted-rewards) + ) +) + +(define-public (calculate-rewards (bond-periods (list 6 uint))) + (let ( + (last-calc (var-get last-reward-compute-height)) + (calculation-height (- (distribution-cycle-to-burn-height (current-distribution-cycle)) + u1 + )) + (cur-reserve (var-get reserve-balance)) + (accrued-rewards (get-new-rewards)) + ) + ;; verify that we are able to compute here + (asserts! (> calculation-height last-calc) + ERR_DISTRIBUTION_ALREADY_COMPUTED + ) + + ;; Verify that all active bonds are included + (try! (assert-all-active-bonds-included bond-periods calculation-height)) + + (let ( + (bond-distributions (try! (fold calculate-bond-rewards bond-periods + (ok { + last-bond-stx-value-ratio: none, + available-rewards: accrued-rewards, + last-bond-index: none, + calculation-height: calculation-height, + }) + ))) + (remaining-rewards (get available-rewards bond-distributions)) + (new-reserve (/ (* remaining-rewards RESERVE_RATIO) u10000)) + (stx-staker-rewards (- remaining-rewards new-reserve)) + (stx-cycle (burn-height-to-reward-cycle calculation-height)) + (cycle-staked-ustx (get-total-shares-staked-for-cycle stx-cycle false)) + (current-rewards-per-ustx (get-rewards-per-token-for-cycle stx-cycle false)) + (prev-accounted-rewards (var-get last-accounted-rewards-only)) + (new-rewards-per-ustx (if (is-eq cycle-staked-ustx u0) + ;; if there are no stx staked, we have a problem + u0 + (/ (* stx-staker-rewards PRECISION) cycle-staked-ustx) + )) + (next-rewards-per-ustx (+ current-rewards-per-ustx new-rewards-per-ustx)) + ) + (print { + topic: "calculate-rewards", + bond-periods: bond-periods, + calculation-height: calculation-height, + remaining-rewards: remaining-rewards, + accrued-rewards: accrued-rewards, + stx-staker-rewards: stx-staker-rewards, + stx-cycle: stx-cycle, + cycle-staked-ustx: cycle-staked-ustx, + next-rewards-per-ustx: next-rewards-per-ustx, + }) + (var-set reserve-balance (+ cur-reserve new-reserve)) + (var-set last-reward-compute-height calculation-height) + (var-set last-accounted-rewards-only + (+ prev-accounted-rewards (- accrued-rewards new-reserve)) + ) + (map-set rewards-per-token-for-cycle { + index: stx-cycle, + is-bond: false, + } + next-rewards-per-ustx + ) + (ok true) + ) + ) +) + +(define-private (calculate-bond-rewards + (bond-index uint) + (accumulator-res (response { + ;; Used to ensure that the list of bonds are sorted correctly + last-bond-stx-value-ratio: (optional uint), + ;; Used as a tie-breaker in the case of bonds with the same + ;; stx-value-ratio + last-bond-index: (optional uint), + ;; How much rewards are available to be distributed + available-rewards: uint, + calculation-height: uint, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (bond (unwrap! (map-get? protocol-bonds bond-index) ERR_BOND_NOT_FOUND)) + (total-sats (get-total-shares-staked-for-cycle bond-index true)) + (available-rewards (get available-rewards accumulator)) + ;; How much sBTC the bond is supposed to earn per calculation, + ;; which is (totalSats * apy) / 48 + (target-yield (/ (/ (* total-sats (get target-rate bond)) u10000) u48)) + ;; If there is enough to cover the target yield, use that. Otherwise, + ;; this bond gets the remaining rewards. + (earned (if (>= available-rewards target-yield) + target-yield + available-rewards + )) + (stx-value-ratio (get stx-value-ratio bond)) + (current-rewards-per-token (get-rewards-per-token-for-cycle bond-index true)) + ;; Prevent divide-by-zero + (new-rewards-per-token (if (is-eq total-sats u0) + u0 + (/ (* earned PRECISION) total-sats) + )) + (calculation-height (get calculation-height accumulator)) + (bond-start-height (bond-period-to-burn-height bond-index)) + (bond-end-height (bond-period-to-burn-height (+ bond-index u6))) + ) + ;; Verify that we're paying out bonds in the right order + (match (get last-bond-stx-value-ratio accumulator) + last-ratio + (asserts! + ;; In a tie-breaker, we still want deterministic results. + ;; Thus, enforce that the earlier bond period comes first + (if (is-eq stx-value-ratio last-ratio) + ;; Note that < prevents the same bond period from + ;; being included twice + (> bond-index + (unwrap-panic (get last-bond-index accumulator)) + ) + (<= stx-value-ratio last-ratio) + ) + ERR_INVALID_BOND_PERIOD_ORDERING + ) + ;; When `none`, this is the first bond we're processing + true + ) + + (map-set rewards-per-token-for-cycle { + is-bond: true, + index: bond-index, + } + (+ current-rewards-per-token new-rewards-per-token) + ) + + (asserts! + (and + (> calculation-height bond-start-height) + (<= calculation-height bond-end-height) + ) + ERR_BOND_NOT_ACTIVE + ) + + (print { + topic: "bond-distribution", + bond-index: bond-index, + target-yield: target-yield, + earned: earned, + }) + + (ok { + last-bond-stx-value-ratio: (some stx-value-ratio), + last-bond-index: (some bond-index), + available-rewards: (- available-rewards earned), + calculation-height: calculation-height, + }) + ) +) + +;; Get the total amount of rewards earned since the last +;; rewards snapshot. +;; +;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` +(define-read-only (get-earned + (signer principal) + (index uint) + (is-bond bool) + ) + (let ( + (shares (get-signer-shares-staked-for-cycle signer index is-bond)) + (rpt-current (get-rewards-per-token-for-cycle index is-bond)) + (rpt-paid (get-signer-rewards-per-token-paid-for-cycle signer index is-bond)) + (pending (get-signer-pending-rewards-for-cycle signer index is-bond)) + (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) + ) + (+ pending newly-earned) + ) +) + +(define-public (claim-rewards + (bond-periods (list 6 uint)) + (reward-cycle uint) + ) + (let ( + (signer contract-caller) + (stx-rewards (update-claimable-rewards signer reward-cycle false)) + (bond-rewards (fold update-claimable-bond-rewards bond-periods { + signer: signer, + total: u0, + bond-rewards: (list), + })) + (bond-totals (get total bond-rewards)) + (total-rewards (+ (get earned stx-rewards) bond-totals)) + (prev-accrued-rewards (var-get last-accounted-rewards-only)) + ) + (asserts! (> total-rewards u0) ERR_NO_CLAIMABLE_REWARDS) + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" total-rewards + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer total-rewards tx-sender signer none + )) + )) + ;; Update contract reward snapshot to prevent issues in next calculation + (var-set last-accounted-rewards-only + (- prev-accrued-rewards total-rewards) + ) + + (print { + topic: "claim-rewards", + stx-rewards: stx-rewards, + bond-rewards: (get bond-rewards bond-rewards), + bond-totals: bond-totals, + total-rewards: total-rewards, + }) + (ok { + stx-rewards: stx-rewards, + bond-rewards: (get bond-rewards bond-rewards), + bond-totals: bond-totals, + total-rewards: total-rewards, + }) + ) +) + +;; For the provided args, calculate the total newly claimable rewards for the signer. +;; Then, update state to reflect this amount as claimed. +;; +;; Returns the newly claimable amount. Does NOT transfer funds out. +(define-private (update-claimable-rewards + (signer principal) + (index uint) + (is-bond bool) + ) + (let ((earned (crystallize-rewards signer index is-bond))) + ;; After crystallization, all earnings live in pending. + ;; Zero out pending since we're about to pay it. + (map-set signer-pending-rewards-for-cycle { + is-bond: is-bond, + index: index, + signer: signer, + } + u0 + ) + earned + ) +) + +(define-private (update-claimable-bond-rewards + (bond-index uint) + (accumulator { + signer: principal, + total: uint, + bond-rewards: (list 6 + { + earned: uint, + bond-index: uint, + rewards-per-token: uint, + } + ), + }) + ) + (let ((rewards-info (update-claimable-rewards (get signer accumulator) bond-index true))) + { + signer: (get signer accumulator), + total: (+ (get total accumulator) (get earned rewards-info)), + bond-rewards: (concat + (unwrap-panic (as-max-len? (get bond-rewards accumulator) u5)) + (list (merge rewards-info { bond-index: bond-index })) + ), + } + ) +) + +;; Update all earned-but-unclaimed rewards for a signer, and update the snapshot +;; (signer-rewards-per-token-paid) for the signer. +;; +;; This MUST be called before any update to `signer-shares-staked-for-cycle`, +;; because changes to that state will effect rewards calculations. +(define-private (crystallize-rewards + (signer principal) + (index uint) + (is-bond bool) + ) + (let ( + (earned (get-earned signer index is-bond)) + (rewards-per-token (get-rewards-per-token-for-cycle index is-bond)) + ) + (map-set signer-pending-rewards-for-cycle { + is-bond: is-bond, + index: index, + signer: signer, + } + earned + ) + (map-set signer-rewards-per-token-paid-for-cycle { + is-bond: is-bond, + index: index, + signer: signer, + } + rewards-per-token + ) + { + earned: earned, + rewards-per-token: rewards-per-token, + } + ) +) + +(define-read-only (assert-all-active-bonds-included + (bond-periods (list 6 uint)) + (calculation-height uint) + ) + (let ( + (calc-cycle (burn-height-to-reward-cycle calculation-height)) + (first-bond-cycle (var-get first-bond-period-cycle)) + (latest-bond-index (if (<= calc-cycle first-bond-cycle) + u0 + (/ (- calc-cycle first-bond-cycle) BOND_GAP_CYCLES) + )) + ) + (try! (fold assert-active-bond-included (list u0 u1 u2 u3 u4 u5) + (ok { + latest-bond-index: latest-bond-index, + calculation-height: calculation-height, + bond-periods: bond-periods, + }) + )) + (ok true) + ) +) + +(define-private (assert-active-bond-included + (offset uint) + (acc-res (response { + latest-bond-index: uint, + calculation-height: uint, + bond-periods: (list 6 uint), + } + uint + )) + ) + (let ( + (acc (try! acc-res)) + (latest-bond-index (get latest-bond-index acc)) + ) + (if (> offset latest-bond-index) + (ok acc) + (let ((bond-index (- latest-bond-index offset))) + (if (is-bond-active-at-height bond-index + (get calculation-height acc) + ) + (begin + (asserts! + (get found + (fold match-uint-in-list (get bond-periods acc) { + needle: bond-index, + found: false, + }) + ) + ERR_ACTIVE_BOND_NOT_INCLUDED + ) + (ok acc) + ) + (ok acc) + ) + ) + ) + ) +) + +;; helper to check if a list contains a value +(define-private (match-uint-in-list + (item uint) + (acc { + needle: uint, + found: bool, + }) + ) + { + needle: (get needle acc), + found: (or (get found acc) (is-eq item (get needle acc))), + } +) + +;; TODO: private fn to transfer funds from reserve +;; (define-private (transfer-from-reserve (amount uint) (recipient uint))) + +;;; Signer key authorization functions + +(define-public (grant-signer-key + (signer-key (buff 33)) + (signer-manager principal) + (auth-id uint) + (signer-sig (buff 65)) + ) + (begin + (asserts! + (is-none (map-get? used-signer-key-grants { + signer-key: signer-key, + signer-manager: signer-manager, + auth-id: auth-id, + })) + ERR_SIGNER_KEY_GRANT_USED + ) + + (asserts! + (is-eq + (unwrap! + (secp256k1-recover? + (get-signer-grant-message-hash signer-manager auth-id) + signer-sig + ) + ERR_INVALID_SIGNATURE_RECOVER + ) + signer-key + ) + ERR_INVALID_SIGNATURE_PUBKEY + ) + + (asserts! + (map-insert used-signer-key-grants { + signer-key: signer-key, + signer-manager: signer-manager, + auth-id: auth-id, + } + true + ) + ERR_SIGNER_KEY_GRANT_USED + ) + + (map-set signer-key-grants { + signer-key: signer-key, + signer-manager: signer-manager, + } + true + ) + + (ok true) + ) +) + +;; Revoke a signer key grant for a staker. Only the Stacks principal +;; associated with `signer-key` can call this function. +;; +;; Returns a boolean indicating whether the signer key grant existed. +(define-public (revoke-signer-grant + (signer-manager principal) + (signer-key (buff 33)) + ) + (begin + ;; Validate that `tx-sender` has the same pubkey hash as `signer-key` + (asserts! + (is-eq + (unwrap-panic (principal-construct? + (if is-in-mainnet + STACKS_ADDR_VERSION_MAINNET + STACKS_ADDR_VERSION_TESTNET + ) + (hash160 signer-key) + )) + tx-sender + ) + ERR_UNAUTHORIZED + ) + (ok (map-delete signer-key-grants { + signer-key: signer-key, + signer-manager: signer-manager, + })) + ) +) + +;; Construct the message hash for validating a signer key grant. Unlike [get-signer-key-message-hash], +;; this message hash does not include `max-amount`, `period`, or `reward-cycle`. The topic is always `"grant-authorization"`. +;; The `pox-addr` field is optional. When `none`, it means the signer key can be used for any PoX address. +(define-read-only (get-signer-grant-message-hash + (signer-manager principal) + (auth-id uint) + ) + (sha256 (concat SIP018_MSG_PREFIX + (concat (sha256 (unwrap-panic (to-consensus-buff? POX_5_SIGNER_DOMAIN))) + (sha256 (unwrap-panic (to-consensus-buff? { + topic: "grant-authorization", + signer-manager: signer-manager, + auth-id: auth-id, + }))) + ))) +) + +(define-read-only (verify-signer-key-grant + (signer-manager principal) + (signer-key (buff 33)) + ) + (ok (asserts! + (is-some (map-get? signer-key-grants { + signer-key: signer-key, + signer-manager: signer-manager, + })) + ERR_SIGNER_KEY_GRANT_NOT_FOUND + )) +) + +;;; Helper functions + +;; What's the burn height at the start of a given bond index? +(define-read-only (bond-period-to-burn-height (bond-index uint)) + (reward-cycle-to-burn-height (bond-period-to-reward-cycle bond-index)) +) + +;; What reward cycle does a bond index start at? +(define-read-only (bond-period-to-reward-cycle (bond-index uint)) + (+ (var-get first-bond-period-cycle) (* bond-index BOND_GAP_CYCLES)) +) + +;; What's the reward cycle number of the burnchain block height? +;; Will runtime-abort if height is less than the first burnchain block (this is intentional) +(define-read-only (burn-height-to-reward-cycle (height uint)) + (/ (- height (var-get first-burnchain-block-height)) + (var-get pox-reward-cycle-length) + ) +) + +;; What's the burn height at the start of a given reward cycle? +(define-read-only (reward-cycle-to-burn-height (cycle uint)) + (+ (var-get first-burnchain-block-height) + (* cycle (var-get pox-reward-cycle-length)) + ) +) + +;; Get the L1 unlock height for a given reward cycle. +;; This is equal to exactly halfway through the provided cycle. +(define-read-only (reward-cycle-to-unlock-height (cycle uint)) + (+ (reward-cycle-to-burn-height cycle) + (/ (var-get pox-reward-cycle-length) u2) + ) +) + +;; What's the current PoX reward cycle? +(define-read-only (current-pox-reward-cycle) + (burn-height-to-reward-cycle burn-block-height) +) + +;; At a given burn height, what distribution cycle are we in? +;; This is zero-indexed at the first reward-cycle +(define-read-only (burn-height-to-distribution-index (height uint)) + (/ (- height (var-get first-burnchain-block-height)) + (/ (var-get pox-reward-cycle-length) u2) + ) +) + +;; What's the current distribution cycle? +(define-read-only (current-distribution-cycle) + (burn-height-to-distribution-index burn-block-height) +) + +;; The start burn height of a given distribution cycle +(define-read-only (distribution-cycle-to-burn-height (cycle uint)) + (+ (var-get first-burnchain-block-height) + (* cycle (/ (var-get pox-reward-cycle-length) u2)) + ) +) + +;; Are we currently in a prepare phase at the end of `current-cycle`? +(define-read-only (is-in-prepare-phase (current-cycle uint)) + (>= burn-block-height + (- (reward-cycle-to-burn-height (+ current-cycle u1)) + (var-get pox-prepare-cycle-length) + )) +) + +(define-read-only (is-bond-active-at-height + (bond-index uint) + (calculation-height uint) + ) + (let ( + (bond-start-height (bond-period-to-burn-height bond-index)) + (bond-end-height (bond-period-to-burn-height (+ bond-index u6))) + ) + (and + (is-some (map-get? protocol-bonds bond-index)) + (> calculation-height bond-start-height) + (<= calculation-height bond-end-height) + ) + ) +) + +;; Used for PoX parameters discovery +(define-read-only (get-pox-info) + (ok { + min-amount-ustx: SIGNER_SET_MIN_USTX, + reward-cycle-id: (current-pox-reward-cycle), + prepare-cycle-length: (var-get pox-prepare-cycle-length), + first-burnchain-block-height: (var-get first-burnchain-block-height), + reward-cycle-length: (var-get pox-reward-cycle-length), + total-liquid-supply-ustx: stx-liquid-supply, + }) +) + +(define-read-only (get-bond-allowance + (bond-index uint) + (staker principal) + ) + (map-get? protocol-bond-allowances { + bond-index: bond-index, + staker: staker, + }) +) + +;; Get _current_ bond member info +(define-read-only (get-bond-membership (staker principal)) + (match (map-get? protocol-bond-memberships staker) + membership (if (<= + (+ BOND_LENGTH_CYCLES + (bond-period-to-reward-cycle (get bond-index membership)) + ) + (current-pox-reward-cycle) + ) + none + (some membership) + ) + none + ) +) + +;; For a given `stx-value-ratio`, which represents "ustx per 100 sats", +;; and a given `min-ustx-ratio`, which represents a minimum amount +;; of STX that must be locked relative to BTC (in basis points), +;; and a given `sats-amount`, calculate the minimum amount +;; of STX needed to hit `min-ustx-ratio`. +;; +;; This is equal to the value-weighted amount of `sats-amount` multiplied +;; by the percentage of `min-ustx-ratio` in STX terms. +(define-read-only (min-ustx-for-sats-amount + (sats-amount uint) + (stx-value-ratio uint) + (min-ustx-ratio uint) + ) + (/ (* (/ (* stx-value-ratio sats-amount) u100) min-ustx-ratio) u10000) +) + +;; Get the _current_ info for a staker. If their +;; stake has expired, this will return `none`. +(define-read-only (get-staker-info (staker principal)) + (match (map-get? staker-info staker) + info + (if (<= (+ (get first-reward-cycle info) (get num-cycles info)) + (current-pox-reward-cycle) + ) + ;; present, but lock has expired + none + ;; present, and lock has not expired + (some info) + ) + ;; no state at all + none + ) +) + +(define-read-only (get-signer-info (signer principal)) + (map-get? signers signer) +) + +;; Get the total uSTX delegated (through protocol bonds and STX-only +;; staking) to this signer. +(define-read-only (get-amount-delegated-for-signer + (signer principal) + (cycle uint) + ) + (default-to u0 + (map-get? signer-delegated-per-cycle { + cycle: cycle, + signer: signer, + }) + ) +) + +;; Get per-cycle staker signer membership info +(define-read-only (get-signer-cycle-membership + (staker principal) + (cycle uint) + ) + (map-get? staker-signer-cycle-memberships { + staker: staker, + cycle: cycle, + }) +) + +(define-read-only (get-total-sbtc-staked-for-bond (bond-index uint)) + (default-to u0 (map-get? protocol-bonds-total-staked bond-index)) +) + +(define-read-only (get-rewards-per-token-for-cycle + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? rewards-per-token-for-cycle { + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-total-shares-staked-for-cycle + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? total-shares-staked-for-cycle { + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-signer-shares-staked-for-cycle + (signer principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? signer-shares-staked-for-cycle { + index: index, + is-bond: is-bond, + signer: signer, + }) + ) +) + +;; Get the amount of shares staked for a given staker in a certain cycle. +(define-read-only (get-staker-shares-staked-for-cycle + (staker principal) + (index uint) + (is-bond bool) + (signer principal) + ) + (default-to u0 + (map-get? staker-shares-staked-for-cycle { + index: index, + staker: staker, + is-bond: is-bond, + signer: signer, + }) + ) +) + +(define-read-only (get-signer-rewards-per-token-paid-for-cycle + (signer principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? signer-rewards-per-token-paid-for-cycle { + signer: signer, + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-signer-pending-rewards-for-cycle + (signer principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? signer-pending-rewards-for-cycle { + signer: signer, + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-signer-pending-staked-ustx-per-cycle + (signer principal) + (cycle uint) + ) + (default-to u0 + (map-get? signer-pending-staked-ustx-per-cycle { + signer: signer, + cycle: cycle, + }) + ) +) + +(define-read-only (get-last-reward-compute-height) + (var-get last-reward-compute-height) +) + +(define-read-only (get-reserve-balance) + (var-get reserve-balance) +) + +(define-read-only (get-total-sbtc-staked) + (var-get total-sbtc-staked) +) + +(define-read-only (get-last-accounted-rewards-only) + (var-get last-accounted-rewards-only) +) + +(define-read-only (get-ustx-delegated-for-cycle (reward-cycle uint)) + (default-to u0 (map-get? ustx-delegated-per-cycle reward-cycle)) +) + +;; How many uSTX are staked? - Required to be named this to +;; work with `chainstate.get_total_ustx_stacked` +(define-read-only (get-total-ustx-stacked (reward-cycle uint)) + (get-ustx-delegated-for-cycle reward-cycle) +) + +(define-read-only (check-pox-lock-period (lock-period uint)) + (and + (>= lock-period u1) + (<= lock-period MAX_NUM_CYCLES) + ) +) + +(define-read-only (get-protocol-bond (bond-index uint)) + (map-get? protocol-bonds bond-index) +) + +;; Returns the expected L1 unlock height for a given bond index. +;; This is equal to 1/2 of a reward cycle before the end of the bond period. +(define-read-only (get-bond-l1-unlock-height (bond-index uint)) + (- (bond-period-to-burn-height (+ bond-index u6)) + (/ (var-get pox-reward-cycle-length) u2) + ) +) + +;;; Contract caller allowances + +(define-read-only (check-caller-allowed) + (ok (asserts! + (or + (is-eq tx-sender contract-caller) + (match (unwrap! + (map-get? allowance-contract-callers { + sender: tx-sender, + contract-caller: contract-caller, + }) + ERR_UNAUTHORIZED_CALLER + ) + expiration (< burn-block-height expiration) + true + ) + ) + ERR_UNAUTHORIZED_CALLER + )) +) + +;; Revoke contract-caller authorization to call stacking methods +(define-public (disallow-contract-caller (caller principal)) + (begin + (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED_CALLER) + (ok (map-delete allowance-contract-callers { + sender: tx-sender, + contract-caller: caller, + })) + ) +) + +;; Give a contract-caller authorization to call stacking methods +;; normally, stacking methods may only be invoked by _direct_ transactions +;; (i.e., the tx-sender issues a direct contract-call to the stacking methods) +;; by issuing an allowance, the tx-sender may call through the allowed contract +(define-public (allow-contract-caller + (caller principal) + (until-burn-ht (optional uint)) + ) + (begin + (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED_CALLER) + (ok (map-set allowance-contract-callers { + sender: tx-sender, + contract-caller: caller, + } + until-burn-ht + )) + ) +) + +;;; Cycle-based Linked List functions + +;; First item in the linked list of stakers +(define-map signer-set-ll-first-for-cycle + uint + principal +) +;; Last item in the linked list of stakers +(define-map signer-set-ll-last-for-cycle + uint + principal +) + +;; Linked list of all stakers for a cycle +(define-map signer-set-ll-for-cycle + { + cycle: uint, + signer: principal, + } + { + prev: (optional principal), + next: (optional principal), + } +) + +(define-read-only (get-signer-set-last-item-for-cycle (cycle uint)) + (map-get? signer-set-ll-last-for-cycle cycle) +) + +(define-read-only (get-signer-set-first-item-for-cycle (cycle uint)) + (map-get? signer-set-ll-first-for-cycle cycle) +) + +(define-read-only (get-signer-set-item-for-cycle + (signer principal) + (cycle uint) + ) + (map-get? signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + }) +) + +(define-read-only (get-signer-set-next-item-for-cycle + (signer principal) + (cycle uint) + ) + (match (map-get? signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + }) + item (get next item) + none + ) +) + +(define-read-only (get-signer-set-prev-item-for-cycle + (signer principal) + (cycle uint) + ) + (match (map-get? signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + }) + item (get prev item) + none + ) +) + +(define-read-only (signer-set-contains-for-cycle + (signer principal) + (cycle uint) + ) + (is-some (map-get? signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + })) +) + +(define-private (add-signer-to-set-for-cycle + (signer principal) + (cycle uint) + ) + (let ((last-item (map-get? signer-set-ll-last-for-cycle cycle))) + ;; Todo: remove this and guard in a higher-level fn + (asserts! + (not (is-some (map-get? signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + }))) + ERR_ALREADY_STAKED + ) + + (match last-item + last-signer (let ((last-node (unwrap-panic (map-get? signer-set-ll-for-cycle { + cycle: cycle, + signer: last-signer, + })))) + (map-set signer-set-ll-for-cycle { + cycle: cycle, + signer: last-signer, + } { + prev: (get prev last-node), + next: (some signer), + }) + (map-set signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + } { + prev: (some last-signer), + next: none, + }) + ) + (begin + ;; This is the first item + (map-set signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + } { + prev: none, + next: none, + }) + (map-set signer-set-ll-first-for-cycle cycle signer) + ) + ) + + (map-set signer-set-ll-last-for-cycle cycle signer) + (ok true) + ) +) + +(define-private (remove-staker-from-set-for-cycle + (signer principal) + (cycle uint) + ) + (let ( + (node (unwrap! + (map-get? signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + }) + ERR_NOT_STAKING + )) + (prev-item (get prev node)) + (next-item (get next node)) + ) + (match prev-item + prev-signer + (map-set signer-set-ll-for-cycle { + cycle: cycle, + signer: prev-signer, + } { + prev: (get prev + (unwrap-panic (map-get? signer-set-ll-for-cycle { + signer: prev-signer, + cycle: cycle, + })) + ), + next: next-item, + }) + ;; this is the first item + (match next-item + next + (map-set signer-set-ll-first-for-cycle cycle next) + ;; no previous or next - this is the only item + (begin + (map-delete signer-set-ll-last-for-cycle cycle) + (map-delete signer-set-ll-first-for-cycle cycle) + ) + ) + ) + + (match next-item + next-signer (map-set signer-set-ll-for-cycle { + cycle: cycle, + signer: next-signer, + } { + prev: prev-item, + next: (get next + (unwrap-panic (map-get? signer-set-ll-for-cycle { + signer: next-signer, + cycle: cycle, + })) + ), + }) + (match prev-item + prev-signer + (map-set signer-set-ll-last-for-cycle cycle prev-signer) + ;; This is the only item - we've already handled this, though + true + ) + ) + (map-delete signer-set-ll-for-cycle { + cycle: cycle, + signer: signer, + }) + (ok true) + ) +) + +;;;; Clarity-Bitcoin helpers + +;; Parse a Bitcoin block header. +;; Returns a tuple structured as folowed on success: +;; (ok { +;; version: uint, ;; block version, +;; parent: (buff 32), ;; parent block hash, +;; merkle-root: (buff 32), ;; merkle root for all this block's transactions +;; timestamp: uint, ;; UNIX epoch timestamp of this block, in seconds +;; nbits: uint, ;; compact block difficulty representation +;; nonce: uint ;; PoW solution +;; }) +(define-read-only (parse-block-header (headerbuff (buff 80))) + (let ( + (ctx { + txbuff: headerbuff, + index: u0, + }) + (parsed-version (try! (read-uint32 ctx))) + (parsed-parent-hash (try! (read-hashslice (get ctx parsed-version)))) + (parsed-merkle-root (try! (read-hashslice (get ctx parsed-parent-hash)))) + (parsed-timestamp (try! (read-uint32 (get ctx parsed-merkle-root)))) + (parsed-nbits (try! (read-uint32 (get ctx parsed-timestamp)))) + (parsed-nonce (try! (read-uint32 (get ctx parsed-nbits)))) + ) + (ok { + version: (get uint32 parsed-version), + parent: (get hashslice parsed-parent-hash), + merkle-root: (get hashslice parsed-merkle-root), + timestamp: (get uint32 parsed-timestamp), + nbits: (get uint32 parsed-nbits), + nonce: (get uint32 parsed-nonce), + }) + ) +) + +;; Reads the next four bytes from txbuff as a little-endian 32-bit integer, and updates the index. +;; Returns (ok { uint32: uint, ctx: { txbuff: (buff 4096), index: uint } }) on success. +;; Returns ERR_READ_TX_OUT_OF_BOUNDS if we read past the end of txbuff +(define-read-only (read-uint32 (ctx { + txbuff: (buff 4096), + index: uint, +})) + (let ( + (data (get txbuff ctx)) + (base (get index ctx)) + ) + (ok { + uint32: (buff-to-uint-le (unwrap-panic (as-max-len? + (unwrap! (slice? data base (+ base u4)) ERR_READ_TX_OUT_OF_BOUNDS) + u4 + ))), + ctx: { + txbuff: data, + index: (+ u4 base), + }, + }) + ) +) + +;; Reads a little-endian hash -- consume the next 32 bytes, and reverse them. +;; Returns (ok { hashslice: (buff 32), ctx: { txbuff: (buff 4096), index: uint } }) on success, and updates the index. +;; Returns ERR_READ_TX_OUT_OF_BOUNDS if we read past the end of txbuff. +(define-read-only (read-hashslice (old-ctx { + txbuff: (buff 4096), + index: uint, +})) + (let ( + (slice-start (get index old-ctx)) + (target-index (+ u32 slice-start)) + (txbuff (get txbuff old-ctx)) + (hash-le (unwrap-panic (as-max-len? + (unwrap! (slice? txbuff slice-start target-index) + ERR_READ_TX_OUT_OF_BOUNDS + ) + u32 + ))) + ) + (ok { + hashslice: (reverse-buff32 hash-le), + ctx: { + txbuff: txbuff, + index: target-index, + }, + }) + ) +) + +(define-read-only (reverse-buff32 (input (buff 32))) + (unwrap-panic (as-max-len? + (concat + (reverse-buff16 (unwrap-panic (as-max-len? (unwrap-panic (slice? input u16 u32)) u16))) + (reverse-buff16 (unwrap-panic (as-max-len? (unwrap-panic (slice? input u0 u16)) u16))) + ) + u32 + )) +) + +(define-private (reverse-buff16 (input (buff 16))) + (unwrap-panic (slice? (unwrap-panic (to-consensus-buff? (buff-to-uint-le input))) u1 u17)) +) +(define-read-only (get-bc-h-hash (bh uint)) + (get-burn-block-info? header-hash bh) +) + +;; Verify that a block header hashes to a burnchain header hash at a given height. +;; Returns true if so; false if not. +;; +;; TODO: remove the `is-in-mainnet` check, instead use proper mocks +(define-read-only (verify-block-header + (headerbuff (buff 80)) + (expected-block-height uint) + ) + (match (get-burn-block-info? header-hash expected-block-height) + bhh (if is-in-mainnet + (is-eq bhh (reverse-buff32 (sha256 (sha256 headerbuff)))) + true + ) + false + ) +) + +;; Get the txid of a transaction, but little-endian. +;; This is the reverse of what you see on block explorers. +(define-read-only (get-reversed-txid (tx (buff 100000))) + (sha256 (sha256 tx)) +) + +;; TODO: replace with clarity built-ins +(define-private (verify-merkle-proof + ;; #[allow(unused_binding)] + (leaf-hash (buff 32)) + ;; #[allow(unused_binding)] + (root-hash (buff 32)) + ;; #[allow(unused_binding)] + (tx-index uint) + ;; #[allow(unused_binding)] + (tx-count uint) + ;; #[allow(unused_binding)] + (leaf-hashes (list 14 (buff 32))) + ) + true +) + +;; TODO: replace with clarity built-ins +(define-private (get-bitcoin-tx-output? + (tx-bytes (buff 100000)) + ;; #[allow(unused_binding)] + (output-index uint) + ;; TODO: remove when built-in exists + ;; #[allow(unused_binding)] + (amount uint) + ;; TODO: remove when built-in exists + ;; #[allow(unused_binding)] + (script (buff 34)) + ) + (if true + (ok { + amount: amount, + script: script, + txid: (get-reversed-txid tx-bytes), + }) + (err u1) ;; indeterminate type otherwise + ) +) + +;;; Lock script helpers + +;; Contruct an L1 lockup script +(define-read-only (construct-lockup-script + (staker principal) + (unlock-burn-height uint) + (unlock-bytes (buff 683)) + (early-unlock-bytes (buff 683)) + ) + (concat (push-script-bytes (unwrap-panic (to-consensus-buff? staker))) + (concat 0x7563 ;; OP_DROP, OP_IF + (concat (push-c-script-num unlock-burn-height) + (concat 0xb175 ;; OP_CHECKLOCKTIMEVERIFY, OP_DROP + (concat (push-script-bytes unlock-bytes) + (concat 0x67 ;; OP_ELSE + (concat (push-script-bytes early-unlock-bytes) + (concat (push-script-bytes unlock-bytes) 0x68 + ;; OP_ENDIF + )) + )) + )) + )) +) + +;; Construct the p2wsh output script for a L1 lockup address +(define-read-only (construct-lockup-output-script + (staker principal) + (unlock-burn-height uint) + (unlock-bytes (buff 683)) + (early-unlock-bytes (buff 683)) + ) + (concat 0x0020 + (sha256 (construct-lockup-script staker unlock-burn-height unlock-bytes + early-unlock-bytes + )) + ) +) + +;; Convert a u8 or u16 to a little-endian byte buffer, +;; ONLY FOR n < 0xffff +(define-read-only (uint-to-buff-le (n uint)) + (unwrap-panic (as-max-len? + (unwrap-panic (slice? (unwrap-panic (to-consensus-buff? n)) + (if (< n u256) + u16 + u17 + ) u17 + )) + u2 + )) +) + +;; Construct the correct script for pushing bytes into a Bitcoin script. +;; +;; If len < 76, just push the length +;; If len < 256, push PUSHDATA1, then the little-endian length +;; If len < 65535 (0xffff), push PUSHDATA2, then the U16LE-encoded length +(define-read-only (push-script-bytes (bytes (buff 1024))) + (let ((byte-length (len bytes))) + (concat + (if (< byte-length u76) + (uint-to-buff-le byte-length) + (if (< byte-length u256) + (concat 0x4c (uint-to-buff-le byte-length)) + (concat 0x4d (uint-to-buff-le byte-length)) + ) + ) + bytes + ) + ) +) + +(define-read-only (serialize-c-script-num (n uint)) + (unwrap-panic (as-max-len? + (if (is-eq n u0) + 0x + (let ( + (bytes (unwrap-panic (to-consensus-buff? n))) + (b0 (unwrap-panic (slice? bytes u16 u17))) + (b1 (unwrap-panic (slice? bytes u15 u16))) + (b2 (unwrap-panic (slice? bytes u14 u15))) + ) + (if (< n u128) + b0 + (if (< n u256) + (concat b0 0x00) + (if (< n u32768) + (concat b0 b1) + (if (< n u65536) + (concat b0 (concat b1 0x00)) + (concat b0 (concat b1 b2)) + ) + ) + ) + ) + ) + ) + u5 + )) +) + +(define-read-only (push-c-script-num (n uint)) + (if (is-eq n u0) + 0x00 + (if (<= n u16) + (unwrap-panic (as-max-len? + (unwrap-panic (slice? (unwrap-panic (to-consensus-buff? (+ u80 n))) u16 u17)) + u1 + )) + (push-script-bytes (serialize-c-script-num n)) + ) + ) +) diff --git a/stacking/deployments/default.simnet-plan.yaml b/stacking/deployments/default.simnet-plan.yaml new file mode 100644 index 0000000..d6dccfb --- /dev/null +++ b/stacking/deployments/default.simnet-plan.yaml @@ -0,0 +1,97 @@ +id: 0 +name: Simulated deployment, used as a default for `clarinet console`, `clarinet test` and `clarinet check` +network: simnet +genesis: + wallets: + - name: deployer + address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_1 + address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_10 + address: ST3FFKYTTB975A3JC3F99MM7TXZJ406R3GKE6JV56 + balance: '200000000000000' + sbtc-balance: '1000000000' + - name: wallet_2 + address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_3 + address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_4 + address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_5 + address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_6 + address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_7 + address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_8 + address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_9 + address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 + balance: '100000000000000' + sbtc-balance: '1000000000' + contracts: + - genesis + - lockup + - bns + - cost-voting + - costs + - pox + - costs-2 + - pox-2 + - costs-3 + - pox-3 + - pox-4 + - signers + - signers-voting + - costs-4 +plan: + batches: + - id: 0 + transactions: + - transaction-type: emulated-contract-publish + contract-name: sbtc-registry + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: sbtc-token + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: sbtc-withdrawal + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar + clarity-version: 3 + epoch: '3.0' + - id: 1 + transactions: + - transaction-type: emulated-contract-publish + contract-name: pox-5 + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/pox-5.clar + clarity-version: 4 + - transaction-type: emulated-contract-publish + contract-name: pox-5-signer + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/pox-5-signer.clar + clarity-version: 4 + epoch: '3.3' diff --git a/stacking/deployments/sbtc.devnet-plan.yaml b/stacking/deployments/sbtc.devnet-plan.yaml new file mode 100644 index 0000000..f5ffc6d --- /dev/null +++ b/stacking/deployments/sbtc.devnet-plan.yaml @@ -0,0 +1,34 @@ +id: 0 +name: sBTC Devnet deployment +network: devnet +stacks-node: http://localhost:20443 +bitcoin-node: http://devnet:devnet@localhost:18443 +plan: + batches: + - id: 0 + transactions: + - transaction-type: requirement-publish + contract-id: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry + remap-sender: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + remap-principals: + SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + cost: 112090 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar + clarity-version: 3 + - transaction-type: requirement-publish + contract-id: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + remap-sender: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + remap-principals: + SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + cost: 47590 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar + clarity-version: 3 + - transaction-type: requirement-publish + contract-id: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal + remap-sender: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + remap-principals: + SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + cost: 122640 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar + clarity-version: 3 + epoch: '3.0' diff --git a/stacking/flood.ts b/stacking/flood.ts index c2fc5aa..0b1bf8c 100644 --- a/stacking/flood.ts +++ b/stacking/flood.ts @@ -1,19 +1,15 @@ -import { StacksTestnet } from '@stacks/network'; import { StackingClient } from '@stacks/stacking'; import { - TransactionVersion, getAddressFromPrivateKey, - getNonce, - makeSTXTokenTransfer, broadcastTransaction, makeRandomPrivKey, - StacksTransaction, makeContractDeploy, makeContractCall, - tupleCV, uintCV, AnchorMode, PostConditionMode, + StacksTransactionWire, + fetchNonce, } from '@stacks/transactions'; import { readFileSync } from 'fs'; import { config } from 'dotenv'; @@ -22,20 +18,21 @@ if (process.argv.slice(2).length > 0) { config({ path: './tx-broadcaster.env' }); } import { bytesToHex } from '@stacks/common'; -import { logger, parseEnvInt, contractsApi, accountsApi } from './common'; +import { logger, parseEnvInt, network, apiClient, nodeUrl } 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 network = new StacksTestnet({ url }); const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0'); const bootstrapperKey = process.env.BOOTSTRAPPER_KEY!; const bootstrapper = { privKey: bootstrapperKey, - stxAddress: getAddressFromPrivateKey(bootstrapperKey, TransactionVersion.Testnet), + stxAddress: getAddressFromPrivateKey(bootstrapperKey, network), }; -const client = new StackingClient(bootstrapper.stxAddress, network); +const client = new StackingClient({ + address: bootstrapper.stxAddress, + network, +}); const floodContract = readFileSync('./flooder.clar', { encoding: 'utf-8' }); @@ -48,8 +45,8 @@ const flooders: { privKey: string; stxAddress: string; nonce: bigint }[] = []; for (let i = 0; i < NUM_FLOODERS; i++) { const privKey = makeRandomPrivKey(); flooders.push({ - privKey: bytesToHex(privKey.data), - stxAddress: getAddressFromPrivateKey(privKey.data, TransactionVersion.Testnet), + privKey, + stxAddress: getAddressFromPrivateKey(privKey, network), nonce: BigInt(0), }); } @@ -58,7 +55,7 @@ let hasSentToFlooders = false; const floodContractDeployer = bootstrapper.stxAddress; async function bootstrapFlooders() { - const nonce = await getNonce(bootstrapper.stxAddress, network); + const nonce = await fetchNonce({ address: bootstrapper.stxAddress, network }); logger.info('Bootstrapping flooders'); // sync iterate let i = 0n; @@ -80,7 +77,6 @@ async function bootstrapFlooders() { network, nonce: nonce + i, senderKey: bootstrapper.privKey, - anchorMode: AnchorMode.Any, codeBody: contractBody, }); await broadcast(bootstrapTx, bootstrapper.stxAddress); @@ -94,7 +90,6 @@ async function bootstrapFlooders() { contractName: 'flood', codeBody: floodContract, fee: 3000000, - anchorMode: 'any', network, postConditionMode: PostConditionMode.Allow, }), @@ -109,10 +104,9 @@ async function bootstrapFlooders() { async function isContractDeployed(address: string) { try { - const result = await contractsApi.getContractSource({ - contractAddress: address, - contractName: 'flood', - }); + const url = `${nodeUrl}/v2/contracts/${address.replace('.', '/')}/source`; + const res = await fetch(url); + const result = (await res.json()) as { source: string }; return !!result.source; } catch (e) { return false; @@ -129,11 +123,18 @@ async function run() { async function flood() { const accountFloods = flooders.map(async (flooder, n) => { - // const nonce = await getNonce(flooder.stxAddress, network); - const nonces = accountsApi.getAccountNonces({ - principal: flooder.stxAddress, + const { data } = await apiClient.GET('/extended/v1/address/{principal}/nonces', { + params: { + path: { + principal: flooder.stxAddress, + }, + }, }); - const nonce = ((await nonces).last_executed_tx_nonce ?? -1) + 1; + if (!data) { + logger.error(`No nonces found for ${flooder.stxAddress}`); + return; + } + const nonce = (data.last_executed_tx_nonce ?? -1) + 1; logger.info(`Flooder ${n} has nonce ${nonce.toString()}`); // return { ...account, nonce }; let txFloods = new Array(TX_PER_FLOOD).fill(0).map(async (_, i) => { @@ -145,7 +146,6 @@ async function flood() { functionArgs: [uintCV(1), uintCV(2), uintCV(3)], senderKey: flooder.privKey, nonce: nonce + i, - anchorMode: 'any', network, fee: 10000, }); @@ -156,11 +156,14 @@ async function flood() { await Promise.all(accountFloods); } -async function broadcast(tx: StacksTransaction, sender?: string) { +async function broadcast(tx: StacksTransactionWire, sender?: string) { const txType = tx.payload.payloadType; const label = sender ? accountLabel(sender) : 'Unknown'; - const broadcastResult = await broadcastTransaction(tx, network); - if (broadcastResult.error) { + const broadcastResult = await broadcastTransaction({ + transaction: tx, + network, + }); + if ('error' in broadcastResult) { logger.error({ ...broadcastResult, account: label }, `Error broadcasting ${txType}`); return false; } else { @@ -185,7 +188,7 @@ async function waitForNakamoto() { break; } } catch (error) { - if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) { + 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); diff --git a/stacking/monitor.ts b/stacking/monitor.ts index 467aa37..3ea281a 100644 --- a/stacking/monitor.ts +++ b/stacking/monitor.ts @@ -1,15 +1,15 @@ +import { bitcoinRPC } from './btc-helpers.js'; import { accounts, nodeUrl, waitForSetup, EPOCH_30_START, didCrossPreparePhase, - blocksApi, parseEnvInt, - txApi, logger, -} from './common'; -import { Transaction, ContractCallTransaction } from '@stacks/stacks-blockchain-api-types'; + WALLET_NAME, + apiClient, +} from './common.js'; let lastBurnHeight = 0; let lastStxHeight = 0; @@ -25,24 +25,49 @@ const monitorInterval = parseEnvInt('MONITOR_INTERVAL') ?? 2; logger.debug('Exit from monitor?', EXIT_FROM_MONITOR); -async function getTransactions(): Promise { - let res = await txApi.getTransactionsByBlock({ - heightOrHash: 'latest', +async function getTransactions() { + const { data } = await apiClient.GET('/extended/v2/blocks/{height_or_hash}/transactions', { + params: { + path: { + height_or_hash: 'latest', + }, + }, }); - let txs = res.results as Transaction[]; - return txs.filter(tx => { + if (!data) { + return []; + } + return data.results.filter(tx => { return tx.tx_type === 'contract_call'; - }) as ContractCallTransaction[]; + }); +} + +async function getBtcStakerBalance() { + const balance = await bitcoinRPC('getbalance', [], WALLET_NAME); + return balance; +} + +async function getLatestBlock() { + try { + const { data } = await apiClient.GET('/extended/v2/blocks/{height_or_hash}', { + params: { + path: { + height_or_hash: 'latest', + }, + }, + }); + return data; + } catch (error) { + return null; + } } async function getInfo() { - let { client } = accounts[0]; - const [poxInfo, blockInfo, txs] = await Promise.all([ + let { client } = accounts[0]!; + const [poxInfo, blockInfo, txs, btcStakerBalance] = await Promise.all([ client.getPoxInfo(), - blocksApi.getBlock({ - heightOrHash: 'latest', - }), + getLatestBlock(), getTransactions(), + getBtcStakerBalance(), ]); const { reward_cycle_id } = poxInfo; return { @@ -50,6 +75,7 @@ async function getInfo() { blockInfo, nextCycleId: reward_cycle_id + 1, txs, + btcStakerBalance, }; } @@ -79,22 +105,23 @@ async function loop() { try { const { poxInfo, blockInfo, ...info } = await getInfo(); let { reward_cycle_id, current_burnchain_block_height } = poxInfo; - let { height } = blockInfo; + const height = blockInfo?.height ?? 0; let showBurnMsg = false; let showPrepareMsg = false; let showCycleMsg = false; let showStxBlockMsg = false; - let burnHeightDate = new Date(blockInfo.burn_block_time * 1000); - let burnHeightTimeAgo = (new Date().getTime() - burnHeightDate.getTime()) / 1000; + const burnBlockTimeMs = (blockInfo?.burn_block_time ?? 0) * 1000; + const burnHeightTimeAgo = (Date.now() - burnBlockTimeMs) / 1000; const loopLog = logger.child({ height, burnHeight: current_burnchain_block_height, // burnHeightTime: cycle: reward_cycle_id, - txCount: blockInfo.tx_count, + txCount: blockInfo?.tx_count, rewardCycle: reward_cycle_id, lastBurnBlock: `${burnHeightTimeAgo.toFixed(0)}s ago`, - burnHash: blockInfo.burn_block_hash, + burnHash: blockInfo?.burn_block_hash, + btcStakerBalance: info.btcStakerBalance, }); if (current_burnchain_block_height && current_burnchain_block_height !== lastBurnHeight) { @@ -145,6 +172,7 @@ async function loop() { if (current_burnchain_block_height === EPOCH_30_START) { loopLog.info('Starting Nakamoto'); } + // loopLog.info({ poxInfo }); } if (showPrepareMsg) { loopLog.info( @@ -166,7 +194,7 @@ async function loop() { } } - if (!showBurnMsg && showStxBlockMsg && blockInfo.burn_block_height >= EPOCH_30_START) { + if (!showBurnMsg && showStxBlockMsg && (blockInfo?.burn_block_height ?? 0) >= EPOCH_30_START) { loopLog.info({ lastStxBlockDiff: lastStxBlockDiff / 1000 }, 'Nakamoto block'); } if (showStxBlockMsg && info.txs.length > 0) { diff --git a/stacking/package.json b/stacking/package.json index fcc7be4..7e26b50 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -5,31 +5,40 @@ "type": "module", "scripts": { "format": "prettier --write *.ts", - "flood": "dotenvx run -f ./tx-broadcaster.env -- npx tsx flood.ts" + "flood": "dotenvx run -f ./tx-broadcaster.env -- npx tsx flood.ts", + "typecheck": "tsc --noEmit" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { + "@clarigen/core": "^4.1.5", + "@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": "7.8.2", - "@stacks/common": "6.11.4-pr.36558cf.0", - "@stacks/encryption": "6.11.4-pr.36558cf.0", - "@stacks/network": "6.11.4-pr.36558cf.0", - "@stacks/stacking": "6.11.4-pr.36558cf.0", + "@stacks/blockchain-api-client": "8.15.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": "6.11.4-pr.36558cf.0", + "@stacks/transactions": "7.4.0", "dotenv": "^16.4.5", "pino": "^8.19.0", "pino-pretty": "^10.3.1" }, "devDependencies": { + "@clarigen/cli": "^4.1.5", "@dotenvx/dotenvx": "^0.26.0", "@stacks/prettier-config": "^0.0.10", - "tsx": "4.7.1" + "@total-typescript/tsconfig": "^1.0.4", + "tsx": "4.7.1", + "typescript": "^6.0.2", + "vitest": "^4.1.3" }, "prettier": "@stacks/prettier-config", "resolutions": { - "@stacks/network": "6.11.4-pr.36558cf.0" + "@stacks/network": "7.4.0" } } diff --git a/stacking/pox-5-helpers.ts b/stacking/pox-5-helpers.ts new file mode 100644 index 0000000..4245d24 --- /dev/null +++ b/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/stacking/settings/Devnet.toml b/stacking/settings/Devnet.toml new file mode 100644 index 0000000..6e75cb2 --- /dev/null +++ b/stacking/settings/Devnet.toml @@ -0,0 +1,79 @@ +[network] +name = "devnet" + +[accounts.deployer] +mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +balance = 100_000_000_000_000 +# secret_key: 753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601 +# stx_address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM +# btc_address: mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH + +[accounts.wallet_1] +mnemonic = "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild" +balance = 100_000_000_000_000 +# secret_key: 7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801 +# stx_address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 +# btc_address: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC + +[accounts.wallet_2] +mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital" +balance = 100_000_000_000_000 +# secret_key: 530d9f61984c888536871c6573073bdfc0058896dc1adfe9a6a10dfacadc209101 +# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG +# btc_address: muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG + +[accounts.wallet_3] +mnemonic = "cycle puppy glare enroll cost improve round trend wrist mushroom scorpion tower claim oppose clever elephant dinosaur eight problem before frozen dune wagon high" +balance = 100_000_000_000_000 +# secret_key: d655b2523bcd65e34889725c73064feb17ceb796831c0e111ba1a552b0f31b3901 +# stx_address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC +# btc_address: mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7 + +[accounts.wallet_4] +mnemonic = "board list obtain sugar hour worth raven scout denial thunder horse logic fury scorpion fold genuine phrase wealth news aim below celery when cabin" +balance = 100_000_000_000_000 +# secret_key: f9d7206a47f14d2870c163ebab4bf3e70d18f5d14ce1031f3902fbbc894fe4c701 +# stx_address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND +# btc_address: mg1C76bNTutiCDV3t9nWhZs3Dc8LzUufj8 + +[accounts.wallet_5] +mnemonic = "hurry aunt blame peanut heavy update captain human rice crime juice adult scale device promote vast project quiz unit note reform update climb purchase" +balance = 100_000_000_000_000 +# secret_key: 3eccc5dac8056590432db6a35d52b9896876a3d5cbdea53b72400bc9c2099fe801 +# stx_address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB +# btc_address: mweN5WVqadScHdA81aATSdcVr4B6dNokqx + +[accounts.wallet_6] +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" +balance = 100_000_000_000_000 +# secret_key: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 +# stx_address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 +# btc_address: mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt + +[accounts.wallet_7] +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" +balance = 100_000_000_000_000 +# secret_key: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 +# stx_address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ +# btc_address: n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7 + +[accounts.wallet_8] +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" +balance = 100_000_000_000_000 +# secret_key: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 +# stx_address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP +# btc_address: n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw + +[accounts.wallet_9] +mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +balance = 100_000_000_000_000 +# secret_key: de433bdfa14ec43aa1098d5be594c8ffb20a31485ff9de2923b2689471c401b801 +# stx_address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 +# btc_address: mjSrB3wS4xab3kYqFktwBzfTdPg367ZJ2d + +[accounts.wallet_10] +mnemonic = "kiss denial decade slide spawn medal twist lamp evidence economy torch alter witness paper rule snack cushion hill sugar fury public innocent almost divide" +balance = 200_000_000_000_000 +# secret_key: 5b897659452b9f3642be69aee75dc3cc84b2386d55ece1312affdbb80a3b2a7d01 +# stx_address: ST3FFKYTTB975A3JC3F99MM7TXZJ406R3GKE6JV56 +# btc_address: n1qwmgbzf1YeHDW6cTxxEwuqnjbqauvKJ1 diff --git a/stacking/stacking.ts b/stacking/stacking.ts index 36a1f84..e3aca00 100644 --- a/stacking/stacking.ts +++ b/stacking/stacking.ts @@ -8,7 +8,7 @@ import { waitForSetup, logger, burnBlockToRewardCycle, -} from './common'; +} from './common.js'; const randInt = () => crypto.randomInt(0, 0xffffffffffff); const stackingInterval = parseEnvInt('STACKING_INTERVAL', true); @@ -21,7 +21,7 @@ let startTxFee = stackingFee; const getNextTxFee = () => startTxFee++; async function run() { - const poxInfo = await accounts[0].client.getPoxInfo(); + 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( diff --git a/stacking/tests/helpers.test.ts b/stacking/tests/helpers.test.ts new file mode 100644 index 0000000..9a114a9 --- /dev/null +++ b/stacking/tests/helpers.test.ts @@ -0,0 +1,6 @@ +import { test, expect } from 'vitest'; +import { burnBlockToRewardCycle } from '../common.js'; + +test('burnBlockToRewardCycle', () => { + expect(burnBlockToRewardCycle(250)).toBe(13); +}); \ No newline at end of file diff --git a/stacking/tsconfig.json b/stacking/tsconfig.json new file mode 100644 index 0000000..c9735b6 --- /dev/null +++ b/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/stacking/tx-broadcaster.ts b/stacking/tx-broadcaster.ts index 7f2bdba..055ea0d 100644 --- a/stacking/tx-broadcaster.ts +++ b/stacking/tx-broadcaster.ts @@ -1,40 +1,42 @@ -import { StacksTestnet } from '@stacks/network'; import { StackingClient } from '@stacks/stacking'; import { - TransactionVersion, getAddressFromPrivateKey, - getNonce, makeSTXTokenTransfer, broadcastTransaction, - StacksTransaction, + StacksTransactionWire, + fetchNonce } from '@stacks/transactions'; -import { logger, CHAIN_ID } from './common'; +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 network = new StacksTestnet({ url }); -network.chainId = CHAIN_ID; const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0'); const accounts = process.env.ACCOUNT_KEYS!.split(',').map(privKey => ({ privKey, - stxAddress: getAddressFromPrivateKey(privKey, TransactionVersion.Testnet), + stxAddress: getAddressFromPrivateKey(privKey, network), })); -const client = new StackingClient(accounts[0].stxAddress, 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 getNonce(account.stxAddress, network); + 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]; + const sender = accountNonces[0]!; + const recipient = accountNonces[1]!; logger.info( `Sending stx-transfer from ${sender.stxAddress} (nonce=${sender.nonce}) to ${recipient.stxAddress}` @@ -47,16 +49,18 @@ async function run() { network, nonce: sender.nonce, fee: 300, - anchorMode: 'any', }); await broadcast(tx, sender.stxAddress); } -async function broadcast(tx: StacksTransaction, sender?: string) { +async function broadcast(tx: StacksTransactionWire, sender?: string) { const txType = tx.payload.payloadType; const label = sender ? accountLabel(sender) : 'Unknown'; - const broadcastResult = await broadcastTransaction(tx, network); - if (broadcastResult.error) { + const broadcastResult = await broadcastTransaction({ + transaction: tx, + network, + }); + if ('error' in broadcastResult) { logger.error({ ...broadcastResult, account: label }, `Error broadcasting ${txType}`); return false; } else { @@ -81,7 +85,7 @@ async function waitForNakamoto() { break; } } catch (error) { - if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) { + 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); diff --git a/stacks-krypton-miner.toml b/stacks-krypton-miner.toml index 772d200..7820c2f 100644 --- a/stacks-krypton-miner.toml +++ b/stacks-krypton-miner.toml @@ -18,6 +18,7 @@ mine_microblocks = false microblock_frequency = 1000 # mine_microblocks = true # max_microblocks = 10 +pox_5_sbtc_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-token" [miner] first_attempt_time_ms = 180_000 @@ -122,6 +123,14 @@ start_height = $STACKS_32_HEIGHT 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"