buildRegisterForBond
Builds an unsigned contract call to pox-5 register-for-bond, which enrolls the sender in a protocol bond. The sender locks STX and either proves an L1 BTC lockup or moves sBTC into pox-5.
Usage
import {
buildRegisterForBond,
fetchBond,
fetchEligibleRegisterForBond,
fetchPoxInfo,
minUstxForSatsAmount,
} from '@stacks/bitcoin-staking';
import {
Pc,
TransactionSigner,
broadcastTransaction,
fetchNonce,
privateKeyToAddress,
privateKeyToPublic,
publicKeyToHex,
} from '@stacks/transactions';
const network = 'mainnet';
const privateKey = process.env.STAKER_KEY!;
const staker = privateKeyToAddress(privateKey, network);
const signerManager = 'SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR.my-signer-manager';
const bondIndex = 2;
const poxInfo = await fetchPoxInfo({ network });
const bond = await fetchBond({ bondIndex, network });
if (!bond) throw new Error(`bond ${bondIndex} is not set up`);
const sbtcSats = 100_000_000n; // 1 BTC in sats
const amountUstx = minUstxForSatsAmount({
sats: sbtcSats,
stxValueRatio: bond.stxValueRatio,
minUstxRatioBps: bond.minUstxRatioBps,
});
const lockup = { kind: 'sbtc', sbtcSats } as const;
const check = await fetchEligibleRegisterForBond({
bondIndex,
staker,
amountUstx,
lockup,
signerManager,
poxInfo,
network,
});
if (!check.ok) throw new Error(`register-for-bond would fail: ${check.reasons}`);
const tx = await buildRegisterForBond({
bondIndex,
signerManager,
amountUstx,
lockup,
publicKey: publicKeyToHex(privateKeyToPublic(privateKey)),
fee: 10_000n, // micro-STX
nonce: await fetchNonce({ address: staker, network }),
network,
postConditions: [
Pc.principal(staker).willSendEq(amountUstx).ustxToLock(),
Pc.principal(staker).willSendLte(sbtcSats).ft(poxInfo.sbtcContract, 'sbtc-token'),
],
});
new TransactionSigner(tx).signOrigin(privateKey);
const result = await broadcastTransaction({ transaction: tx, network });For an L1 BTC lockup, pass lockup: { kind: 'btc', outputs, unlockBytes }, where each entry of outputs comes from buildLockProof or buildLockProofFromBlock, and unlockBytes is the staker-signature script the lockup was built with in buildLockScript. Without an sBTC rollover, only the ustxToLock post condition applies.
Notes
Send it before the bond starts (
ERR_BOND_ALREADY_STARTED (u43)) and outside the prepare phase, the lastprepareCycleLengthBitcoin blocks of a reward cycle, 100 on mainnet (ERR_STAKE_IN_PREPARE_PHASE (u47)) (pox-5.clar L642-L842).The bond must exist (
ERR_BOND_NOT_FOUND (u7)). The sender must be on its allowlist (ERR_NOT_ALLOWLISTED (u11)) and stake no more sats than their allowance (ERR_TOO_MUCH_SATS (u10)).amountUstxmust be at least minUstxForSatsAmount for the staked sats, and the sender's total STX balance, locked plus unlocked, must coveramountUstx. Both fail withERR_INSUFFICIENT_STX (u8).The signer-manager must be registered (
ERR_SIGNER_NOT_FOUND (u23)) with an active signer key grant (ERR_SIGNER_KEY_GRANT_NOT_FOUND (u17)). Itsvalidate-stake!can reject the registration with its own error code.An STX-only stake still locked in the bond's first reward cycle fails with
ERR_ALREADY_STAKED (u19). A bond membership that overlaps the new bond fails withERR_ALREADY_REGISTERED (u9). Rolling over from an ending bond is allowed only from half a reward cycle before that bond ends (ERR_ROLLOVER_TOO_EARLY (u48), L3004-L3025).fetchEligibleRegisterForBond dry-runs these checks. It does not run the signer-manager's
validate-stake!, or the L1 merkle proof, lockup script, amount and transaction-parse checks.With
kind: 'btc', the builder throws before building ifoutputsis empty, has more than 10 entries, or an output has more than 14leafHashes, the contract's list limits. pox-5 then verifies each output against Bitcoin chainstate and fails withERR_INVALID_BTC_HEADER (u40),ERR_INVALID_MERKLE_PROOF (u41),ERR_INVALID_LOCKUP_SCRIPT (u42),ERR_INVALID_LOCKUP_AMOUNT (u45),ERR_DUPLICATE_LOCKUP_OUTPOINT (u46), orERR_INVALID_UNLOCK_HEIGHT (u52)whenunlockBurnHeightis below the bond's minimum fromget-bond-l1-unlock-heightor at or above 500,000,000 (L2031-L2113).With
kind: 'sbtc', pox-5 transfers from the sender only the difference betweensbtcSatsand the sBTC it already custodies for the sender's current bond, the fullsbtcSatswhen there is none (L1938-L1979). That is why the sBTC post condition is an upper bound.The builder attaches no post conditions, and the default
denymode aborts the transaction withabort_by_post_conditionunless post conditions cover every asset movement. Read the sBTC token contract from fetchPoxInfo instead of hardcoding it.On a rollover, pox-5 moves only the difference between the sBTC it custodies for the sender's ending bond and the sBTC the new bond needs (
0forkind: 'btc') (L1938-L1979). Read the custodied amount with fetchStakerCustodiedSbtc. When it exceeds what the new bond needs, pox-5 refunds the difference from its own principal, so indenymode addPc.principal(poxInfo.contractId).willSendEq(refund).ft(poxInfo.sbtcContract, 'sbtc-token')withrefundset to that difference.A txid from broadcastTransaction confirms submission only. The call can still fail on-chain: read the mined transaction's result and pass an
(err uN)to parsePox5Error.
Signature
The transaction fields come from TxParams.
Returns
Promise<StacksTransactionWire>
Resolves to an unsigned StacksTransactionWire that calls pox-5 register-for-bond. Sign it with TransactionSigner, then send it with broadcastTransaction.
Parameters
args.bondIndex (required)
Type:
number
Index of the bond to join.
args.signerManager (required)
Type:
string
Contract principal of the signer-manager, which implements signer-manager-trait.
args.amountUstx (required)
Type:
IntegerType
STX to lock for the bond, in micro-STX. The lock runs 12 reward cycles from the bond's first reward cycle.
args.lockup (required)
Type: BondLockup
{ kind: 'sbtc', sbtcSats } to stake sBTC, in sats, or { kind: 'btc', outputs, unlockBytes } to prove L1 BTC lockup outputs. The builder encodes the sBTC form as (err sbtcSats) and the BTC form as (ok { outputs, staker-unlock-bytes }), as pox-5 expects.
args.signerCalldata (optional)
Type:
Uint8Array | string
Up to 500 bytes, as bytes or hex, that pox-5 forwards unread to the signer-manager's validate-stake!. Omitted, the contract receives none. buildSignerCalldata encodes an L1 BTC payout election for signer-managers that read that format.
args.fee (required)
Type:
IntegerType
Transaction fee in micro-STX.
args.nonce (required)
Type:
IntegerType
Nonce of the origin account. Read it with fetchNonce.
args.network (required)
Type:
StacksNetworkName | StacksNetwork
A name such as 'mainnet' or 'testnet', or a StacksNetwork object. Sets the contract address (the network's boot address, SP000000000000000000002Q6VF78 on mainnet) and the transaction version and chain ID.
args.publicKey (required for a single-sig origin)
Type:
string
Hex public key of the account that signs the transaction.
args.publicKeys and args.numSignatures (required for a multisig origin)
Type:
PublicKey[]andnumber
Pass these instead of publicKey to build for an M-of-N multisig origin. This package defaults useNonSequentialMultiSig to true; pass false for the sequential hash mode. Pass address to check the key order against the multisig address.
args.postConditions (optional)
Type:
PostCondition[]
Post conditions to attach. The builder adds none.
args.postConditionMode (optional)
Type:
PostConditionModeName
'allow', 'deny' or 'originator'. Defaults to 'deny'.
Was this helpful?