# stacks > Typed Stacks client for TypeScript. By secondlayer. ## Getting started Install `@secondlayer/stacks` and create a client against a Stacks network. ```bash bun add @secondlayer/stacks ``` ```ts import { createPublicClient, http } from "@secondlayer/stacks"; import { mainnet } from "@secondlayer/stacks/chains"; const client = createPublicClient({ chain: mainnet, transport: http(), }); const balance = await client.getBalance({ address: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", }); ``` Clients and transports live at the package root (not separate export subpaths). The sections below are the module notes for each. ### Clients Client factories and composable action decorators. ### Public Client (Read-Only) ```typescript import { createPublicClient, http, mainnet } from "@secondlayer/stacks"; const client = createPublicClient({ chain: mainnet, transport: http(), }); const balance = await client.getBalance({ address: "SP2J6..." }); const height = await client.getBlockHeight(); ``` ### Wallet Client ```typescript import { createWalletClient, http, mainnet } from "@secondlayer/stacks"; import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; const client = createWalletClient({ account: privateKeyToAccount("0x..."), chain: mainnet, transport: http(), }); const { txid } = await client.sendTransaction({ transaction: signedTx }); ``` ### Multi-Sig Client ```typescript import { createMultiSigClient, http, mainnet } from "@secondlayer/stacks"; const client = createMultiSigClient({ publicKeys: [pk1, pk2, pk3], signaturesRequired: 2, chain: mainnet, transport: http(), }); ``` ### Extending with Extensions ```typescript import { bns } from "@secondlayer/stacks/bns"; import { pox } from "@secondlayer/stacks/pox"; const client = createPublicClient({ chain: mainnet, transport: http(), }).extend(bns()).extend(pox()); await client.bns.resolveName("alice.btc"); await client.pox.getPoxInfo(); ``` ### Custom Decorators ```typescript const myActions = (client) => ({ myCustomAction: () => client.readContract({ ... }), }); const client = createPublicClient({ ... }).extend(myActions); ``` ### Transports Transport layer for communicating with Stacks nodes. ### HTTP (Default) ```typescript import { http } from "@secondlayer/stacks"; // Uses chain's default RPC URL const transport = http(); // Custom URL const transport = http("https://my-node.example.com"); // With options const transport = http("https://my-node.example.com", { apiKey: "my-api-key", timeout: 30_000, retryCount: 3, retryDelay: 1_000, }); ``` Non-2xx responses throw a typed `HttpRequestError` (`.status` attached) instead of resolving with the error body — check `e.status` rather than reading `client.request(...)`'s return value for failure. Retries cover `5xx`, network errors, and `429`. ### WebSocket ```typescript import { webSocket } from "@secondlayer/stacks"; const transport = webSocket(); // Custom URL const transport = webSocket("wss://my-node.example.com"); ``` ### Fallback Tries transports in order, falls back on failure. ```typescript import { fallback, http } from "@secondlayer/stacks"; const transport = fallback([ http("https://primary-node.com"), http("https://backup-node.com"), ]); ``` ### Simnet Separate entry. `@stacks/clarinet-sdk` is an optional peer of that entry only. ```typescript import { initSimnet } from "@stacks/clarinet-sdk"; import { createPublicClient } from "@secondlayer/stacks"; import { simnet, simnetChain } from "@secondlayer/stacks/simnet"; const session = await initSimnet("./Clarinet.toml"); const client = createPublicClient({ chain: simnetChain, transport: simnet(session), }); ``` ### Custom ```typescript import { custom } from "@secondlayer/stacks"; const transport = custom({ async request(path, options) { const response = await fetch(`https://my-api.com${path}`, { method: options?.method ?? "GET", body: options?.body ? JSON.stringify(options.body) : undefined, }); return response.json(); }, }); ``` ## Accounts Key derivation and account creation for signing transactions. ### From Private Key ```typescript import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; const account = privateKeyToAccount("0xprivatekey..."); account.address; // "SP2J6..." account.publicKey; // "03ab..." account.sign(hash); // Uint8Array ``` ### From Mnemonic ```typescript import { mnemonicToAccount } from "@secondlayer/stacks/accounts"; const account = mnemonicToAccount("abandon abandon abandon ..."); // Derive a different account index const account2 = mnemonicToAccount("abandon ...", { accountIndex: 1 }); ``` ### Custom Signer ```typescript import { toAccount } from "@secondlayer/stacks/accounts"; const account = toAccount({ address: "SP2J6...", publicKey: "03ab...", sign: async (hash) => { // custom signing logic (HSM, hardware wallet, etc.) return signature; }, }); ``` ### Browser Wallet ```typescript import { providerToAccount } from "@secondlayer/stacks/accounts"; const account = providerToAccount(window.StacksProvider); ``` ## Actions Standalone action functions for tree-shakeable imports. These are the same functions available on clients, but usable without a client instance. ### Public (Read-Only) Actions ```typescript import { getBalance, getNonce, readContract, getBlock, getBlockHeight } from "@secondlayer/stacks/actions"; const balance = await getBalance(client, { address: "SP2J6..." }); const nonce = await getNonce(client, { address: "SP2J6..." }); const height = await getBlockHeight(client); ``` `getAccountHistory`, `getMempoolStats`, and `getNftHoldings` cover paginated tx history, mempool stats, and NFT holdings as first-class, standalone-importable actions. ### Node-Only Reads `getContractSource` and `getRawBlock` hit a stacks-node's raw RPC (`/v2/contracts/source`, `/v2/blocks/{height}`) — not Hiro's extended API and not a tenant proxy, so `client` needs a direct node transport. `getRawBlock` is distinct from `getBlock` (which reads the indexed extended-API shape): it returns node/consensus fields like `index_block_hash` and `miner_txid`. Both return `null` on a 404 or missing data instead of throwing. ```typescript import { getContractSource, getRawBlock } from "@secondlayer/stacks/actions"; const src = await getContractSource(client, { contract: "SP2J6....my-contract" }); const block = await getRawBlock(client, { height: 150_000 }); ``` ### Contract Reads ```typescript import { readContract } from "@secondlayer/stacks/actions"; import { Cl } from "@secondlayer/stacks/clarity"; const result = await readContract(client, { contractAddress: "SP2J6...", contractName: "my-contract", functionName: "get-balance", functionArgs: [Cl.principal("SP3FBR...")], }); ``` ### Typed Contracts ```typescript import { getContract } from "@secondlayer/stacks/actions"; const contract = getContract({ client, address: "SP2J6...", name: "my-contract", abi: MY_ABI, }); // Type-safe reads, calls, map lookups, and unsigned tx builds const balance = await contract.read.getBalance({ account: "SP3FBR..." }); const txid = await contract.call.transfer({ to: "SP3FBR...", amount: 100n }); const entry = await contract.maps.tokenBalances("SP3FBR..."); // value or null // Build an unsigned transaction for a wallet to sign later — never broadcasts. // publicKey defaults to the client account; fee/nonce are auto-resolved when // omitted (see ContractBuildCallOptions for fee/nonce/postConditions/sponsored). const tx = await contract.buildCall.transfer( { to: "SP3FBR...", amount: 100n }, { publicKey }, ); ``` ABIs generated by `sl codegen contracts` are branded with `TypedAbi`, so all four namespaces surface the generated named type aliases in hovers and errors; hand-written `as const` ABIs get the same API via structural inference. ### Wallet Actions ```typescript import { sendTransaction, transferStx, callContract } from "@secondlayer/stacks/actions"; const { txid } = await sendTransaction(client, { transaction: signedTx }); const txid = await transferStx(client, { recipient: "SP2J6...", amount: 1_000_000n, }); ``` ### Simulation ```typescript import { simulateCall, multicall } from "@secondlayer/stacks/actions"; // Dry-run a contract call const result = await simulateCall(client, { contractAddress: "SP2J6...", contractName: "my-contract", functionName: "transfer", functionArgs: [Cl.uint(100)], sender: "SP3FBR...", }); // Batch multiple reads const results = await multicall(client, { calls: [ { contractAddress: "SP2J6...", contractName: "token-a", functionName: "get-balance", functionArgs: [Cl.principal("SP3FBR...")] }, { contractAddress: "SP2J6...", contractName: "token-b", functionName: "get-balance", functionArgs: [Cl.principal("SP3FBR...")] }, ], }); ``` ## Bitcoin SPV Trust-minimized Bitcoin verification for Stacks contracts (SIP-044): build an inclusion proof from your own node, encode it for Clarity, and prove a payment was mined without trusting a third party. The [Bitcoin SPV guide](/guide/bitcoin-spv) covers the model and the reference adapter. This page is the surface. ### Verify a Payment `verifyBitcoinPayment` composes the whole flow: build the proof, prove the tx is mined via the adapter's `was-tx-mined`, decode the target output, assert your expectations. ```typescript import { createPublicClient, http } from "@secondlayer/stacks"; import { mainnet } from "@secondlayer/stacks/chains"; import { verifyBitcoinPayment, esploraSource } from "@secondlayer/stacks/bitcoin"; const client = createPublicClient({ chain: mainnet, transport: http() }); const result = await verifyBitcoinPayment(client, { txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", source: esploraSource({ url: "https://blockstream.info/api" }), vout: 0, expect: { address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", amount: 5_000_000_000n }, }); // { verified: true, mined: true, output: { vout, script, amount, type, address }, proof } ``` | Param | | | ----------------------------- | --------------------------------------------------------------------------------------------- | | `txid` + `source`, or `proof` | Where the proof comes from, or a prepared `SpvProof` | | `vout` | Output index to decode and check | | `expect?` | `{ address?, amount? }`; `verified` is true only when mined and every expectation matches | | `contract?` | Adapter principal. Optional on mainnet (reference adapter resolves); required elsewhere | | `network?` | For address formatting; defaults from `client.chain` | | `authenticateHeader?` | Default `true`: header checked against `get-burn-block-info?`. `false` proves membership only | ### Proof Sources Where proof inputs come from. `buildTxProof` re-checks every claim a source makes, so a wrong or hostile source fails loudly. ```typescript import { bitcoinRpcSource, esploraSource, fallbackProofSource } from "@secondlayer/stacks/bitcoin"; const source = fallbackProofSource([ bitcoinRpcSource({ url: "http://127.0.0.1:8332", auth: { username: "u", password: "p" } }), esploraSource({ url: "https://mempool.space/api" }), ]); ``` | Source | | | ------------------------------------------ | ------------------------------------------------------------------------------- | | `bitcoinRpcSource({ url, auth?, fetch? })` | Your Bitcoin Core node over JSON-RPC (needs `-txindex`). Trustless; the default | | `esploraSource({ url, fetch? })` | Any Esplora-compatible REST API, self-hosted or hosted | | `fallbackProofSource([...])` | Try each in order, return the first success, throw the last error | Both satisfy `ProofSource`; implement it yourself for another backend. ### Build a Proof ```typescript import { buildTxProof } from "@secondlayer/stacks/bitcoin"; const proof = await buildTxProof(source, { txid: "f418...9e16", vout: 0 }); // { // rawTx, // Uint8Array, witness stripped // txidInternal, // 32 bytes, internal byte order // vout, // merkle: { siblings, txIndex, txCount }, // header, // 80 bytes // height, // } ``` Checks performed before returning: the raw tx hashes to `txid`, `txIndex` points at that txid in the block, and the merkle proof folds back to the header's root. Hashes are internal byte order throughout. ### Encode for Clarity The SIP-044 built-ins want flat arguments in internal byte order with `tx-count`, not tree depth. These absorb that. ```typescript import { encodeMerkleProofArgs, decodeTxOutput, parseOutputScript } from "@secondlayer/stacks/bitcoin"; // (leaf, root, tx-index, tx-count, (list 24 (buff 32))) const args = encodeMerkleProofArgs({ leaf: proof.txidInternal, root, proof: proof.merkle }); // get-bitcoin-tx-output? result → { script, amount, txid } const out = decodeTxOutput(resultCV); const spk = parseOutputScript(out.script); // { type: "p2pkh" | "p2sh" | "p2wpkh" | "p2wsh" | "p2tr" | "op_return" | "unknown", data? } ``` ### Verifier Bind to a deployed adapter contract and call its read-onlys with typed proofs. The built-ins only exist at Clarity 6 / Epoch 4.0, so calls succeed on mainnet after activation or a local Clarity-6 devnet. ```typescript import { bitcoinVerifier, getSpvAdapter, spvAdapterPrincipal } from "@secondlayer/stacks/bitcoin"; const adapter = getSpvAdapter("mainnet")!; // { address, name } const verifier = bitcoinVerifier(client, { contract: spvAdapterPrincipal(adapter) }); await verifier.wasTxMined(proof); // true | false; throws on non-canonical header ``` `SPV_ADAPTER_CONTRACTS` pins the reference deployment (`SP2M1DE95TS0QBM4K893X6ST49FFJ53CCX9CYWNVY.spv-adapter` on mainnet, none on testnet). `SPV_ADAPTER_ABI` is exported `as const`. ### Activation ```typescript import { isClarity6Active, getBurnBlockHeight, EPOCH_4_ACTIVATION_BURN_HEIGHT_MAINNET } from "@secondlayer/stacks/bitcoin"; await isClarity6Active(client); // mainnet: height known await isClarity6Active(devnetClient, { activationBurnHeight: 120 }); // elsewhere: supply it await getBurnBlockHeight(client); // /v2/info burn_block_height EPOCH_4_ACTIVATION_BURN_HEIGHT_MAINNET; // 960230 ``` ### Addresses ```typescript import { formatBitcoinAddress, publicKeyToP2wpkhAddress, publicKeyToP2trAddress, taprootTweakPubkey, } from "@secondlayer/stacks/bitcoin"; formatBitcoinAddress(parseOutputScript(script), "mainnet"); // "bc1q..." | undefined for op_return / unknown publicKeyToP2wpkhAddress(compressedPubkey); // "bc1q..." publicKeyToP2trAddress(compressedPubkey, "testnet"); // "tb1p..." (BIP-86 tweak) taprootTweakPubkey(xonly); // 32-byte tweaked x-only key ``` ### Serialization Low-level parsers shared by everything above. Use them to inspect a raw tx or header without a proof. ```typescript import { parseBitcoinTx, stripWitness, bitcoinTxid, parseBlockHeader, blockHash, doubleSha256, reverseBytes, BtcReader, } from "@secondlayer/stacks/bitcoin"; const tx = parseBitcoinTx(rawTx); // { version, hasWitness, inputs, outputs, locktime } const legacy = stripWitness(rawTx); // bytes the txid is computed over bitcoinTxid(rawTx); // internal order bitcoinTxid(rawTx, { display: true }); // explorer order const header = parseBlockHeader(headerBytes); // { version, prevBlock, merkleRoot, time, bits, nonce } blockHash(headerBytes, { display: true }); ``` ### Merkle ```typescript import { buildMerkleProof, merkleRoot, rootFromProof } from "@secondlayer/stacks/bitcoin"; const root = merkleRoot(txidsInternal); const merkle = buildMerkleProof(txidsInternal, txIndex); // { siblings, txIndex, txCount } rootFromProof(txidsInternal[txIndex], merkle); // equals root ``` ### PoX-5 `spvProofToL1LockupOutput` and `buildPox5LockProof` in [pox5](/reference/pox5#lock-proofs) map these proofs onto `register-for-bond`'s lockup shape. ## BNS v2 BNS (Bitcoin Name System) v2 extension for name resolution, registration, and management on Stacks. ### Setup ```typescript import { createPublicClient, createWalletClient, http, mainnet } from "@secondlayer/stacks"; import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; import { bns } from "@secondlayer/stacks/bns"; // Read-only client const client = createPublicClient({ chain: mainnet, transport: http(), }).extend(bns()); // Wallet client (for registration/transfers) const wallet = createWalletClient({ account: privateKeyToAccount("0x..."), chain: mainnet, transport: http(), }).extend(bns()); ``` ### Resolve Names ```typescript // Name -> address const owner = await client.bns.resolveName("alice.btc"); // Address -> primary name const name = await client.bns.getPrimaryName("SP2J6..."); // Check availability const available = await client.bns.canRegister("bob.btc"); // `true` only means the contract call itself resolved to "name unknown" — // network/HTTP failures throw instead of being reported as "available". // Get price (microSTX) const price = await client.bns.getNamePrice("bob.btc"); // Get NFT token ID const id = await client.bns.getNameId("alice.btc"); ``` ### Register Names Two registration paths: #### Fast Claim (instant, snipeable) ```typescript const txid = await wallet.bns.claimFast({ name: "bob.btc", recipient: account.address, }); ``` #### Secure Registration (2-step, front-run proof) ```typescript // Step 1: Preorder (commits salted hash) const { txid, salt } = await wallet.bns.preorder({ name: "bob.btc" }); // Wait ~10 minutes (1 Bitcoin block) // Step 2: Register (reveals name) await wallet.bns.register({ name: "bob.btc", salt }); ``` ### Manage Names ```typescript // Transfer await wallet.bns.transfer({ name: "alice.btc", recipient: "SP3FBR...", }); // Set primary name await wallet.bns.setPrimary({ name: "alice.btc" }); ``` ### Zonefiles ```typescript // Read zonefile const zonefile = await client.bns.getZonefile("alice.btc"); if (zonefile) console.log(new TextDecoder().decode(zonefile)); // `null` only means the contract genuinely has no zonefile set — network/HTTP // failures throw instead of being reported as "no zonefile". // Update zonefile await wallet.bns.updateZonefile({ name: "alice.btc", zonefile: "$ORIGIN alice.btc\n$TTL 3600\n...", }); // Clear zonefile await wallet.bns.revokeZonefile("alice.btc"); ``` ### Namespace All methods accept fully-qualified names (`alice.btc`) or bare names (`alice`, defaults to `.btc`). ## Chains Chain definitions for Stacks networks. ### Predefined Chains ```typescript import { mainnet, testnet, devnet, mocknet } from "@secondlayer/stacks/chains"; // Use with clients const client = createPublicClient({ chain: mainnet, transport: http(), }); ``` ### Custom Chain ```typescript import { defineChain } from "@secondlayer/stacks/chains"; const custom = defineChain({ id: 0x80000000, name: "my-network", network: "testnet", transactionVersion: 0x80, peerNetworkId: 0xfaceb00c, addressVersion: { singleSig: 26, multiSig: 21 }, magicBytes: "T2", bootAddress: "ST000000000000000000002AMW42H", nativeCurrency: { name: "Stacks", symbol: "STX", decimals: 6 }, rpcUrls: { default: { http: ["http://localhost:3999"] }, }, }); ``` ## Clarity Clarity value constructors, serialization, and ABI type system. ### Constructing Values ```typescript import { Cl } from "@secondlayer/stacks/clarity"; Cl.uint(100); // (uint u100) Cl.int(-42); // (int -42) Cl.bool(true); // true Cl.principal("SP2J6..."); // (principal SP2J6...) Cl.contractPrincipal("SP2J6...", "my-contract"); Cl.bufferFromAscii("hello"); // (buff 0x68656c6c6f) Cl.bufferFromHex("deadbeef"); Cl.stringAscii("hello"); // (string-ascii "hello") Cl.stringUtf8("hello"); // (string-utf8 u"hello") Cl.none(); // none Cl.some(Cl.uint(42)); // (some u42) Cl.ok(Cl.uint(42)); // (ok u42) Cl.error(Cl.uint(1)); // (err u1) Cl.list([Cl.uint(1), Cl.uint(2)]); // (list u1 u2) Cl.tuple({ name: Cl.stringAscii("alice"), age: Cl.uint(30) }); ``` ### Serialization ```typescript import { serializeCV, deserializeCV } from "@secondlayer/stacks/clarity"; const hex = serializeCV(Cl.uint(42)); // hex string const value = deserializeCV(hex); // ClarityValue ``` ### Pretty Printing ```typescript import { prettyPrint, cvToJSON, cvToValue } from "@secondlayer/stacks/clarity"; prettyPrint(Cl.uint(42)); // "u42" cvToJSON(Cl.uint(42)); // { type: "uint", value: "42" } cvToValue(Cl.uint(42)); // 42n ``` ### JS Bridge ```typescript import { jsToClarityValue, clarityValueToJS, isClarityValue } from "@secondlayer/stacks/clarity"; // Convert JS → Clarity using ABI type hints const cv = jsToClarityValue("uint128", 42n); // Pre-built ClarityValues pass through unchanged (escape hatch) jsToClarityValue("uint128", Cl.uint(42n)); // Buffer args accept flexible inputs const buff = { buff: { length: 34 } }; jsToClarityValue(buff, new Uint8Array([1, 2])); jsToClarityValue(buff, "0xdeadbeef"); // hex (0x optional) jsToClarityValue(buff, { type: "ascii", value: "hi" }); // ascii | utf8 | hex // Runtime CV guard isClarityValue(Cl.uint(1n)); // true // Convert Clarity → JS const js = clarityValueToJS(abiType, cv); ``` ### ABI Type System ```typescript import type { TypedAbi, ContractTypes, AbiTypesOf } from "@secondlayer/stacks/clarity"; ``` `sl codegen contracts` emits named per-function type aliases plus a `Types` bundle, and brands the generated ABI const with `TypedAbi`. The brand is a phantom property — zero runtime cost — that brand-aware consumers (`getContract`) resolve via `AbiTypesOf` to show the named aliases in hovers and errors. Un-branded `as const` ABIs keep working through structural inference. ### Standard ABIs ```typescript import { SIP010_ABI, SIP009_ABI, SIP013_ABI } from "@secondlayer/stacks/clarity"; // Use with getContract() for typed token interactions const token = getContract({ client, address: "SP2J6...", name: "my-token", abi: SIP010_ABI, }); ``` ## connect/walletconnect Native WalletConnect v2 for Stacks. Separate entry so it tree-shakes when unused. ```ts import { connect, setProvider } from "@secondlayer/stacks/connect"; import { WalletConnectProvider, showModal } from "@secondlayer/stacks/connect/walletconnect"; const wc = new WalletConnectProvider({ projectId: "your-reown-project-id", // from cloud.reown.com metadata: { name: "My App", description: "...", url: "https://myapp.com", icons: [] }, }); // Restore existing session or pair new one if (!wc.restore()) { const { uri, approval } = await wc.pair(); showModal({ wcUri: uri, onClose: () => {} }); await approval; } setProvider(wc); const { addresses } = await connect(); ``` The built-in modal shows browser extension wallets alongside the WC QR code — users pick whichever they prefer. ### Exports `WalletConnectProvider`, `WcSession`, `WcRelay`, `qrSvg`, `showModal`, `hideModal`, and related types (`WcProviderConfig`, `WcMetadata`, `WcPairResult`, `WcSessionSettled`, `WcSessionData`). See also [WalletConnect guide](/guide/walletconnect) and [connect](/reference/connect). ## Connect Browser wallet connection via SIP-030 (Leather, Xverse, etc.). ### Connect Wallet ```typescript import { connect, disconnect, isConnected } from "@secondlayer/stacks/connect"; // Prompt user to connect const { addresses } = await connect(); const stxAddress = addresses.find((a) => a.symbol === "STX"); // Check connection state if (isConnected()) { // ... } // Disconnect disconnect(); ``` ### Wallet Requests ```typescript import { request } from "@secondlayer/stacks/connect"; // Transfer STX await request("stx_transferStx", { recipient: "SP2J6...", amount: "1000000", memo: "coffee", }); // Call contract await request("stx_callContract", { contract: "SP2J6....my-contract", functionName: "transfer", functionArgs: [Cl.uint(100), Cl.principal("SP3FBR...")], }); // Deploy contract await request("stx_deployContract", { name: "my-token", clarityCode: "(define-fungible-token my-token)", }); // Sign message await request("stx_signMessage", { message: "Hello Stacks", }); ``` ### Provider Detection ```typescript import { isWalletInstalled, getProvider, setProvider } from "@secondlayer/stacks/connect"; if (isWalletInstalled()) { const provider = getProvider(); } // Use a custom provider setProvider(myCustomProvider); ``` ## Filters Describe a chain event once, then hand the same filter to Index, Streams, Webhooks, or Subgraphs. `@secondlayer/stacks/filters` normalizes the three things that differ per surface: event-type names (`print` vs `print_event`), amount encoding (`bigint` in code, strings on the wire), and which fields each surface can actually filter on. You write one validated filter; each `to*()` projection emits the exact request shape that surface expects, and refuses to emit one it cannot honor. ### The Four Surfaces This module only matters alongside a Secondlayer instance. If you use `@secondlayer/stacks` purely as a chain client, skip it. The surfaces are [Secondlayer platform](https://secondlayer.tools/docs) products reached through the `@secondlayer/sdk` client (`sl` below); the platform-side write-up is [secondlayer.tools/docs/filters](https://secondlayer.tools/docs/filters). * **Index** pulls decoded history over HTTP, paginated and cursor-based. `sl.index.events.list(f.toIndexParams())`. [Docs →](https://secondlayer.tools/docs/index) * **Streams** consumes events as they land: ordered, resumable, reorg-aware. `sl.streams.events.consume(f.toStreamsParams())`. [Docs →](https://secondlayer.tools/docs/streams) * **Webhooks** push matching events to your URL; a chain trigger is the filter half of a webhook. `sl.webhooks.create({ triggers: [f.toChainTrigger()] })`. [Docs →](https://secondlayer.tools/docs/webhooks) * **Subgraphs** turn event sources plus handlers into a queryable dataset. `defineSubgraph({ sources: { x: f.toSubgraphSource() } })`. [Docs →](https://secondlayer.tools/docs/subgraphs) ### Build a Filter `on.*` has one factory per event type. Fields are validated on construction: principals, contract ids, and asset identifiers that are malformed throw immediately. ```typescript import { on } from "@secondlayer/stacks/filters"; const USDC = "SP3Y2ZSH8P7D50B0VBTSX11S7XSG24M1VB9YFQA4K.token-aeusdc::aeUSDC"; const usdc = on.ftTransfer({ assetIdentifier: USDC, minAmount: 1_000_000n }); const prints = on.print({ contractId: "SP3Y2...token-aeusdc", topic: "transfer" }); const calls = on.contractCall({ contractId: "SP3Y2...token-aeusdc", functionName: "transfer" }); const deposits = on.sbtcDeposit({ minAmount: 100_000n }); ``` `makeChainEventFilter(type, fields)` is the generic form; `on.ftTransfer(f)` is `makeChainEventFilter("ft_transfer", f)`. ### Project to a Surface The same filter feeds all four surfaces. ```typescript import { SecondLayer } from "@secondlayer/sdk"; import { defineSubgraph } from "@secondlayer/subgraphs"; const sl = new SecondLayer({ baseUrl: "https://your-instance.example" }); await sl.index.events.list(usdc.toIndexParams({ limit: 100 })); // pull history await sl.streams.events.consume({ ...usdc.toStreamsParams(), onBatch }); // live firehose await sl.webhooks.create({ name: "usdc", url, triggers: [usdc.toChainTrigger()] }); // push defineSubgraph({ sources: { usdc: usdc.toSubgraphSource() }, schema, handlers }); // dataset ``` | Projection | Output | Notes | | ------------------------- | ---------------------------------------- | -------------------------------------------------- | | `toIndexParams(extra?)` | `{ eventType, ...fields, ...extra }` | `print_event` becomes `print` | | `toStreamsParams(extra?)` | `{ types: [type], ...fields, ...extra }` | `print_event` becomes `print` | | `toChainTrigger()` | `{ type, ...fields }` | `bigint` amounts stringified for JSON | | `toSubgraphSource()` | the spec, `bigint` preserved | keeps `abi` / `prints` literals for handler typing | A surface a member cannot reach is a missing method, which is a compile error, not a runtime surprise: ```typescript const sbtc = on.sbtcDeposit({ minAmount: 100_000n }); sbtc.toChainTrigger(); // ok: { type: "sbtc_deposit", minAmount: "100000" } sbtc.toIndexParams(); // type error: sBTC lifecycle filters are Webhooks-only ``` ### What Throws Where A field the target cannot express throws at projection time, naming the surface that can. ```typescript const usdc = on.ftTransfer({ assetIdentifier: USDC, minAmount: 1_000_000n }); usdc.toIndexParams(); // Error: minAmount cannot be expressed on Index events — amount predicates are // Webhooks/Subgraphs-only; filter client-side on the decoded rows. ``` | Field | Index | Streams | Webhooks | Subgraphs | | ------------------------------------ | ---------------------------------- | ------- | -------- | --------------------------- | | `minAmount` / `maxAmount` | throws | throws | ok | ok | | `topic` (print) | throws | throws | ok | ok | | wildcard `*` in a principal or asset | throws | throws | ok | ok | | `trait` | ok, not with `contractId` | throws | ok | ok, ANDed with `contractId` | | `contractId` as a set | ok | ok | throws | ok | | `factory` | throws | throws | throws | ok | | `caller` | throws (use `index.contractCalls`) | ok | ok | ok | ### Event Types ```typescript import { CHAIN_EVENT_FILTER_TYPES, DECODED_EVENT_TYPES } from "@secondlayer/stacks/filters"; ``` `DECODED_EVENT_TYPES` is the ten-member set Index and Streams share: `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_transfer`, `ft_mint`, `ft_burn`, `nft_transfer`, `nft_mint`, `nft_burn`, `print`. `CHAIN_EVENT_FILTER_TYPES` adds the contract-shaped members (`contract_call`, `contract_deploy`, `print_event`) and the five sBTC lifecycle members (`sbtc_deposit`, `sbtc_withdrawal_create`, `sbtc_withdrawal_accept`, `sbtc_withdrawal_reject`, `sbtc_withdrawal_swept_confirmed`). ### Spec Fields | Factory | Fields | | ---------------------------------------------- | ----------------------------------------------------------------------------------- | | `on.stxTransfer` | `sender`, `recipient`, `minAmount`, `maxAmount` | | `on.stxMint` / `on.stxBurn` / `on.stxLock` | `recipient` / `sender` / `lockedAddress`, `minAmount` | | `on.ftTransfer` | `assetIdentifier`, `sender`, `recipient`, `minAmount`, `trait` | | `on.ftMint` / `on.ftBurn` | `assetIdentifier`, `recipient` / `sender`, `minAmount`, `trait` | | `on.nftTransfer` / `on.nftMint` / `on.nftBurn` | `assetIdentifier`, `sender`, `recipient`, `trait` | | `on.contractCall` | `contractId` (one or up to 20), `functionName`, `caller`, `abi`, `trait`, `factory` | | `on.contractDeploy` | `deployer`, `contractName` | | `on.print` | `contractId` (one or up to 20), `topic`, `prints`, `trait`, `factory` | | `on.sbtcDeposit` | `sender`, `minAmount`, `maxAmount`, `bitcoinTxid`, `requestId` | | `on.sbtcWithdrawalCreate` | `sender`, `minAmount`, `maxAmount`, `requestId` | | `on.sbtcWithdrawalAccept` / `SweptConfirmed` | `requestId`, `sweepTxid` | | `on.sbtcWithdrawalReject` | `requestId` | `abi` and `prints` are type-only decorations: `on.contractCall({ abi })` keeps `event.input` typed inside `defineSubgraph`, and `on.print({ prints })` narrows `event.data` per topic. Both are dropped from every projection except `toSubgraphSource`. ### Validation The same validators the factories use, for form and config code. ```typescript import { assetId, isPrincipal, hasWildcard, assertPrincipalish, assertContractId, assertAssetIdentifier, } from "@secondlayer/stacks/filters"; assetId("SP3Y2...token-aeusdc::aeUSDC"); // narrows to AssetIdentifier, throws if malformed isPrincipal("SP2QEZ06AGJ3RKJPBV14SY1V5BBFNAW33D96YPGZF"); // true hasWildcard("SP3Y2*"); // true; wildcards skip validation and are Webhooks/Subgraphs-only assertContractId("contractId", "SP3Y2...token-aeusdc::aeUSDC"); // throws: that is an asset id, not a contract id ``` ### Round-Tripping Subgraph Sources `fromSubgraphSource` rebuilds a filter from a subgraph-source object. `toSubgraphSource(fromSubgraphSource(s))` deep-equals `s` for every production source, which is how existing subgraph definitions migrate onto the shared vocabulary. ```typescript import { fromSubgraphSource } from "@secondlayer/stacks/filters"; const filter = fromSubgraphSource({ type: "ft_transfer", assetIdentifier: USDC, minAmount: 1_000_000n }); filter.toChainTrigger(); // now also a webhook trigger ``` ## Post-Conditions Fluent builder for Stacks post-conditions. Protects users by asserting expected asset transfers. ### STX ```typescript import { Pc } from "@secondlayer/stacks/postconditions"; // Sender will send exactly 1 STX Pc.principal("SP2J6...").willSendEq(1_000_000).ustx(); // Sender will send at most 5 STX Pc.principal("SP2J6...").willSendLte(5_000_000).ustx(); ``` ### Fungible Tokens ```typescript // Sender will send exactly 100 tokens Pc.principal("SP2J6...") .willSendEq(100) .ft("SP2J6....my-token", "my-token"); ``` ### NFTs ```typescript import { Cl } from "@secondlayer/stacks/clarity"; // Sender will send NFT Pc.principal("SP2J6...") .willSendAsset() .nft("SP2J6....my-nft::my-nft", Cl.uint(1)); // Sender will NOT send NFT Pc.principal("SP2J6...") .willNotSendAsset() .nft("SP2J6....my-nft::my-nft", Cl.uint(1)); // Epoch 3.4: NFT may or may not be sent Pc.principal("SP2J6...") .willMaybeSendAsset() .nft("SP2J6....my-nft::my-nft", Cl.uint(1)); ``` ### Staking / PoX (SIP-045) pox-5 locks STX natively — use a staking post-condition, not `.ustx()`. ```typescript Pc.principal(staker).willSendEq(amountUstx).ustxToLock(); Pc.principal(staker).willPerformPox(); Pc.origin().willNotPerformPox(); ``` `unstake` / `claimRewards` send an amount the contract computes at execution. Use `postConditionMode: "allow"` rather than guessing a bound. Hex PCs (Leather, stacks.js) round-trip: `Pc.fromHex(hex)` / `postConditionToHex(pc)`. ### Comparators | Method | Clarity Equivalent | | ---------------- | ------------------ | | `willSendEq(n)` | `=` | | `willSendGt(n)` | `>` | | `willSendGte(n)` | `>=` | | `willSendLt(n)` | `<` | | `willSendLte(n)` | `<=` | ## PoX Stacking PoX (Proof of Transfer) extension for STX stacking — earn Bitcoin rewards by locking STX. Supports solo stacking and pool delegation via the `pox-4` contract. ### Setup ```typescript import { createPublicClient, createWalletClient, http, mainnet } from "@secondlayer/stacks"; import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; import { pox } from "@secondlayer/stacks/pox"; const client = createPublicClient({ chain: mainnet, transport: http(), }).extend(pox()); const wallet = createWalletClient({ account: privateKeyToAccount("0x..."), chain: mainnet, transport: http(), }).extend(pox()); ``` ### Query Stacking State ```typescript // Network info (cycle, minimum threshold, lengths) const info = await client.pox.getPoxInfo(); console.log(info.minAmountUstx); // minimum to solo stack console.log(info.rewardCycleId); // current cycle // Check if an amount meets the threshold const eligible = await client.pox.canStack(100_000_000_000n); // Stacker info (returns null if not stacking) const stacker = await client.pox.getStackerInfo("SP2J6..."); // Delegation info (returns null if not delegating) const delegation = await client.pox.getDelegationInfo("SP2J6..."); ``` ### Solo Stacking Lock STX directly and earn BTC rewards to your Bitcoin address. ```typescript await wallet.pox.stackStx({ amount: 100_000_000_000n, // 100k STX in microSTX btcAddress: "bc1q...", // BTC reward address lockPeriod: 12, // 1-12 cycles startBurnHeight: 850_000n, // burn height at which stacking begins signerSig: signature, // signer signature (buff 65) signerKey: publicKey, // signer public key (buff 33) maxAmount: 100_000_000_000n, authId: 1n, }); ``` #### Extend Lock ```typescript await wallet.pox.stackExtend({ extendCount: 6, // additional cycles (1-12) btcAddress: "bc1q...", signerSig: signature, signerKey: publicKey, maxAmount: 100_000_000_000n, authId: 2n, }); ``` #### Increase Locked Amount ```typescript await wallet.pox.stackIncrease({ increaseBy: 50_000_000_000n, // additional microSTX signerSig: signature, signerKey: publicKey, maxAmount: 150_000_000_000n, authId: 3n, }); ``` ### Pool Delegation Delegate STX to a pool operator who stacks on your behalf. ```typescript // Delegate to pool await wallet.pox.delegateStx({ amount: 100_000_000_000n, delegateTo: "SP2...", // pool operator address untilBurnHeight: 900_000n, // optional expiry poxAddr: "bc1q...", // optional BTC address restriction }); // Revoke delegation await wallet.pox.revokeDelegateStx(); ``` ### Utilities ```typescript import { parseBtcAddress, burnHeightToRewardCycle, rewardCycleToBurnHeight } from "@secondlayer/stacks/pox"; // Parse any BTC address format to PoX tuple const poxAddr = parseBtcAddress("bc1q..."); // Convert between burn heights and reward cycles const cycle = burnHeightToRewardCycle(info, 850_000n); const height = rewardCycleToBurnHeight(info, 95n); ``` ## PoX-5 Bitcoin Staking `pox5()` client extension for SIP-045 Bitcoin Staking: activation gating, staker reads, every `pox-5` public function as a typed wallet action, plus the pure helpers the contract's math and scripts depend on. The [PoX-5 guide](/guide/pox5) walks the flows end to end. This page is the surface. ### Setup ```typescript import { createPublicClient, createWalletClient, http, mainnet } from "@secondlayer/stacks"; import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; import { pox5 } from "@secondlayer/stacks/pox5"; const client = createPublicClient({ chain: mainnet, transport: http(), }).extend(pox5()); const wallet = createWalletClient({ account: privateKeyToAccount("0x..."), chain: mainnet, transport: http(), }).extend(pox5()); ``` ### Activation Reads `/v2/pox` `contract_versions`, so the same code is right on mainnet, testnet, and a devnet with a custom activation height. ```typescript await client.pox5.isActive(); // true once the burnchain passes the activation height await client.pox5.getActivation(); // { contractId, activationBurnchainBlockHeight, firstRewardCycleId } | undefined await client.pox5.getPoxInfo(); // /v2/pox, typed: cycle length, prepare length, current burn height, ... client.pox5.contractId(); // "SP000000000000000000002Q6VF78.pox-5" on mainnet ``` Standalone equivalents take a client: `isPox5Active(client)`, `getPox5Activation(client)`, `getPoxInfo(client)`, and `assertPox5Active(client)`, which throws naming the activation height (`POX5_ACTIVATION_BURN_HEIGHT_MAINNET`, Bitcoin block 960,230). ### Reads `getStakerState` batches the four reads a UI needs into one request. ```typescript const state = await client.pox5.getStakerState("SP2J6..."); // { stakerInfo, bondMembership, custodiedSbtc, currentCycle } const info = await client.pox5.getStakerInfo("SP2J6..."); // null when not staking const bond = await client.pox5.getProtocolBond(0); const membership = await client.pox5.getBondMembership("SP2J6..."); const custodied = await client.pox5.getStakerCustodiedSbtc("SP2J6..."); const total = await client.pox5.getTotalSbtcStakedForBond(0); const cycle = await client.pox5.getCurrentRewardCycle(); const first = await client.pox5.getFirstRewardCycle(); const earned = await client.pox5.getEarned({ signer: "SP2J6...", rewardCycle: cycle }); ``` Also: `getBondAllowance`, `hasAnnouncedL1EarlyExit`, `getBondL1UnlockHeight`, `getSignerInfo`, `verifySignerKeyGrant`, `getEarnedStakerRewards`, `getLastRewardComputeHeight`, `getTotalSharesStakedForCycle`. Optional records return `null`; nothing throws on a missing row. ### Eligibility Pre-flight checks that mirror the contract's asserts, so a doomed transaction never burns a fee. Each returns `{ ok: true }` or `{ ok: false, reasons: [Pox5ErrorCode, ...] }`. ```typescript const check = await client.pox5.eligibleStake({ staker: "SP2J6...", signerManager: "SP2J6....signer-mgr", amountUstx: 100_000_000_000n, numCycles: 12, startBurnHeight: 960_231, }); if (!check.ok) { for (const code of check.reasons) console.log(describePox5Error(code)?.name); // e.g. "ERR_UNAUTHORIZED" } ``` One per action: `eligibleStake`, `eligibleRegisterForBond`, `eligibleUnstake`, `eligibleUnstakeSbtc`, `eligibleClaimRewards`, `eligibleGrantSignerKey`, `eligibleSetBondAdmin`, `eligiblePauseRewards`. Pure versions with the same names take a client as the first argument. ### Wallet Actions Every `pox-5` public function. All accept `fee` (`min` | `low` | `mid` | `high` or a microSTX amount), `nonce`, `postConditions`, `postConditionMode`; all return a txid. ```typescript const txid = await wallet.pox5.stake({ signerManager: "SP2J6....signer-mgr", amountUstx: 100_000_000_000n, // 100,000 STX numCycles: 12, startBurnHeight: 960_231, fee: "low", }); await wallet.waitForTransactionReceipt({ txid, confirmations: 1 }); ``` | Action | Contract call | | ----------------------------- | --------------------------------- | | `setupBond` | `setup-bond` | | `registerForBond` | `register-for-bond` | | `updateBondRegistration` | `update-bond-registration` | | `stake` | `stake` | | `stakeUpdate` | `stake-update` | | `unstake` | `unstake` | | `unstakeSbtc` | `unstake-sbtc` | | `announceL1EarlyExit` | `announce-l1-early-exit` | | `calculateRewards` | `calculate-rewards` | | `claimRewards` | `claim-rewards` | | `claimStakerRewardsForSigner` | `claim-staker-rewards-for-signer` | | `grantSignerKey` | `grant-signer-key` | | `revokeSignerGrant` | `revoke-signer-grant` | | `setBondAdmin` | `set-bond-admin` | | `setPauseAdmin` | `set-pause-admin` | | `pauseRewards` | `pause-rewards` | `registerForBond` takes the BTC side as a union: custodied sBTC, or SPV-proven L1 lockup outputs. ```typescript // custodied sBTC await wallet.pox5.registerForBond({ bondIndex: 0, signerManager: "SP2J6....signer-mgr", amountUstx: 500_000_000_000n, btcLockup: { sbtcSats: 100_000_000n }, }); // proven L1 lockup, proof built by buildLockProof (below) await wallet.pox5.registerForBond({ bondIndex: 0, signerManager: "SP2J6....signer-mgr", amountUstx: 500_000_000_000n, btcLockup: { l1Outputs: [output], stakerUnlockBytes }, }); ``` ### Lock Proofs Map a SIP-044 SPV proof onto the `L1LockupOutput` shape `register-for-bond` verifies on-chain. Witness data is stripped and the lockup vout is resolved against the P2WSH of your lock script. ```typescript import { esploraSource } from "@secondlayer/stacks/bitcoin"; const output = await wallet.pox5.buildLockProof({ source: esploraSource({ url: "https://blockstream.info/api" }), txid: "f418...9e16", lockScript, // the witness script from buildLockupScript unlockBurnHeight: 987_530n, }); ``` `spvProofToL1LockupOutput` does the mapping for a proof you already hold; `buildPox5LockProof` is the standalone fetch-and-map. ### Lockup Scripts Byte-for-byte ports of the contract's `construct-lockup-script`. The contract compares your L1 output against exactly these bytes. ```typescript import { buildDefaultStakerUnlockBytes, buildLockupScript, buildLockupOutputScript, buildLockupAddress, stakerPreimage, } from "@secondlayer/stacks/pox5"; const stakerUnlockBytes = buildDefaultStakerUnlockBytes(compressedPubkey); // OP_CHECKSIG const opts = { stxAddress: "SP2J6...", unlockBurnHeight: 987_530, stakerUnlockBytes, earlyUnlockBytes, // from the bond's protocol-bonds.early-unlock-bytes }; const script = buildLockupScript(opts); // witness script: CLTV branch + early-exit branch const spk = buildLockupOutputScript(opts); // 0x0020 || sha256(script) const address = buildLockupAddress(opts, "mainnet"); // bc1q...; also "testnet" | "regtest" const preimage = stakerPreimage("SP2J6..."); // 32 bytes the early-exit branch must reveal ``` Lower-level pieces (`pushScriptBytes`, `pushCScriptNum`, `serializeCScriptNum`, `stakerConsensusBuff`, `buildRegisterMetadata`) are exported for custom script tails. ### Reclaiming a Lockup Spend a P2WSH lockup back out once it unlocks (`"locktime"` path) or after `announce-l1-early-exit` with the bond cosigner (`"early-exit"` path, which also needs `cosignerPrivateKey` and `stxAddress`). Builds a complete PSBT; nothing here broadcasts. ```typescript import { reclaim, buildReclaim, computeReclaimSighash, finalizeReclaim } from "@secondlayer/stacks/pox5"; const opts = { path: "locktime" as const, network: "mainnet" as const, utxo: { txid: "ab12...", vout: 0, value: 100_000_000n }, output: { address: "bc1q...", feeSats: 1_200n }, // sweeps value - feeSats lockScript, // the witness script you funded }; // in-process BTC key: one shot const { txHex, txid } = reclaim({ ...opts, stakerPrivateKey }); // HSM / MPC: build, sign the digest elsewhere, finalize const tx = buildReclaim(opts); const sighash = computeReclaimSighash(tx); // sign externally, then tx.addPartialSig / signIdx const { txHex: hex } = finalizeReclaim({ path: "locktime", tx }); ``` ### Signer Grants SIP-018 structured-data signatures authorizing a signer-manager contract to use a signer key. `signSignerGrant` returns RSV order, the `(buff 65)` layout `grant-signer-key` expects. ```typescript import { computeSignerGrantHash, signSignerGrant, verifySignerGrant } from "@secondlayer/stacks/pox5"; const opts = { signerManager: "SP2J6....signer-mgr", authId: 1n, chainId: mainnet.id }; const signerSig = await signSignerGrant(signerAccount, opts); verifySignerGrant({ ...opts, publicKey: signerPubkey, signature: signerSig }); // true computeSignerGrantHash(opts); // byte-identical to get-signer-grant-message-hash ``` `buildSignerCalldata` / `parseSignerCalldata` pack and unpack the calldata a signer-manager contract receives. ### Cycle Math Pure mirrors of the contract's cycle read-onlys. Pass chain parameters from `getPoxInfo()` and `getActivation()`. ```typescript import { burnHeightToRewardCycle, rewardCycleToBurnHeight, bondPhaseAtHeight, bondStatusAtHeight, isInPreparePhase, computeBondUnlockHeight, } from "@secondlayer/stacks/pox5"; const params = { firstBurnchainBlockHeight: 666_050, rewardCycleLength: 2_100 }; burnHeightToRewardCycle(960_231, params); // cycle number rewardCycleToBurnHeight(140, params); // first burn height of a cycle bondPhaseAtHeight(0, 960_231, { ...params, firstBondPeriodCycle }); // "too-early" | "open" | "locked" | "unlocked" isInPreparePhase(960_231, { ...params, prepareCycleLength: 100 }); ``` Also: `bondPeriodToRewardCycle`, `bondPeriodToBurnHeight`, `bondUnlockCycle`, `burnHeightToDistributionIndex`, `currentDistributionCycle`, `distributionCycleToBurnHeight`. Constants: `BOND_LENGTH_CYCLES` (12), `BOND_GAP_CYCLES` (2), `MAX_NUM_CYCLES` (96). ### Bitcoin Addresses The PoX `(version, hashbytes)` tuple in both directions. ```typescript import { parseBtcAddress, stringifyBtcAddress, BtcAddress } from "@secondlayer/stacks/pox5"; parseBtcAddress("bc1q..."); // { version: 4, hashbytes } stringifyBtcAddress({ version: 6, hashbytes }, "mainnet"); // "bc1p..." BtcAddress.parse === parseBtcAddress; // namespaced aliases ``` ### Errors Every `pox-5` error code, named and described. `parsePox5Error` reads the code out of an `(err uN)` result. ```typescript import { parsePox5Error, describePox5Error, Pox5ErrorCode } from "@secondlayer/stacks/pox5"; const code = parsePox5Error(result); // 1 describePox5Error(code!); // { code: 1, name: "ERR_UNAUTHORIZED", description: "..." } Pox5ErrorCode.CannotSetupBondTooSoon; // 2 ``` `POX5_ABI` is the curated `as const` ABI for `getContract` and typed `contract_call` subgraph sources; `POX5_EVENT_TOPICS` lists the print topics. ## sBTC `sbtc()` client extension for the sBTC protocol: token reads, signer-set lookups, unit conversion, and the event types the registry emits. The protocol runs three contracts on mainnet and testnet: `sbtc-token` (SIP-010), `sbtc-deposit` (deposit completion entry point), and `sbtc-registry` (every protocol-state print event). This module pins the current deployments; the testnet address changed in the 2026-08 reset and the old one 404s. ### Setup ```typescript import { createPublicClient, http, mainnet } from "@secondlayer/stacks"; import { sbtc } from "@secondlayer/stacks/sbtc"; const client = createPublicClient({ chain: mainnet, transport: http(), }).extend(sbtc()); ``` Network resolves from `client.chain`: a testnet client reads the testnet contracts without any extra configuration. ### Token Reads All SIP-010 read-onlys on `sbtc-token`. Amounts are satoshis as `bigint`; sBTC has 8 decimals like BTC. ```typescript const supply = await client.sbtc.getTotalSupply(); // 5_000_000_000n const balance = await client.sbtc.getBalance("SP2J6..."); // 12_500_000n await client.sbtc.getName(); // "sBTC" await client.sbtc.getSymbol(); // "sBTC" await client.sbtc.getDecimals(); // 8n await client.sbtc.getTokenUri(); // string | null ``` ### Signer Set The current signer set from `sbtc-registry`. The taproot address is where deposits go; it rotates on `key-rotation`, so read it at deposit time rather than caching it. ```typescript const pubkey = await client.sbtc.getSignersPublicKey(); // Uint8Array, 33-byte compressed aggregate key const address = await client.sbtc.getSignersAddress(); // "bc1p..." on mainnet, "tb1p..." on testnet ``` ### Units ```typescript import { satsToSbtc, sbtcToSats } from "@secondlayer/stacks/sbtc"; satsToSbtc(150_000_000n); // "1.5" satsToSbtc(1n); // "0.00000001" sbtcToSats("1.5"); // 150_000_000n sbtcToSats("0.123456789"); // throws: more than 8 decimal places ``` `satsToSbtc` returns a decimal string, never a float, so it is safe to display or store as-is. ### Bitcoin Identifiers Registry events carry Bitcoin txids as 32-byte buffers and BTC recipients as `(version, hashbytes)` tuples. These decode them. ```typescript import { bitcoinTxidToHex, bitcoinTxidFromHex, validateBitcoinTxid, formatBtcAddress, } from "@secondlayer/stacks/sbtc"; const hex = bitcoinTxidToHex(event.bitcoinTxid); // 64-char hex; throws if not 32 bytes const buf = bitcoinTxidFromHex(hex); // Uint8Array(32) validateBitcoinTxid(buf); // throws on wrong length // withdrawal-create `recipient` tuple → address string const to = formatBtcAddress(event.recipient); // mainnet const toTest = formatBtcAddress(event.recipient, "testnet"); ``` Version bytes follow the SIP-005 PoX map (`SBTC_BTC_ADDRESS_VERSION`): `p2pkh` 0x00 through `p2tr` 0x06. ### Contracts and Constants ```typescript import { SBTC_CONTRACTS, sbtcContractId, SBTC_ASSET_IDENTIFIER_MAINNET, SBTC_EVENT_TOPICS, SBTC_DECIMALS, } from "@secondlayer/stacks/sbtc"; SBTC_CONTRACTS.mainnet.address; // "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4" sbtcContractId("mainnet", "registry"); // "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry" SBTC_ASSET_IDENTIFIER_MAINNET; // "SM3V....sbtc-token::sbtc-token", for post-conditions and filters SBTC_EVENT_TOPICS; // ["completed-deposit", "withdrawal-create", ...] SBTC_DECIMALS; // 8 ``` `SBTC_TOKEN_ABI` is exported `as const` for `getContract` and typed subgraph sources. ### Event Types Typed shapes for every `sbtc-registry` print topic and every `sbtc-token` SIP-005 event, for code that consumes decoded events from Index, Streams, or a subgraph handler. | Type | Topic | Fields | | ----------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------- | | `CompletedDepositEvent` | `completed-deposit` | `bitcoinTxid`, `outputIndex`, `amount`, `burnHash`, `burnHeight`, `sweepTxid` | | `WithdrawalCreateEvent` | `withdrawal-create` | `requestId`, `amount`, `sender`, `recipient`, `blockHeight`, `maxFee` | | `WithdrawalAcceptEvent` | `withdrawal-accept` | `requestId`, `bitcoinTxid`, `signerBitmap`, `outputIndex`, `fee`, `burnHash`, `burnHeight`, `sweepTxid` | | `WithdrawalRejectEvent` | `withdrawal-reject` | `requestId`, `signerBitmap` | | `KeyRotationEvent` | `key-rotation` | `newKeys`, `newAddress`, `newAggregatePubkey`, `newSignatureThreshold` | | `UpdateProtocolContractEvent` | `update-protocol-contract` | `contractType`, `newContract` | `SbtcRegistryEvent` is the union, discriminated on `topic`; `SbtcEventByTopic<"withdrawal-create">` picks one. Token events are `SbtcTokenTransferEvent`, `SbtcTokenMintEvent`, `SbtcTokenBurnEvent`, discriminated on `type`. ```typescript import type { SbtcRegistryEvent } from "@secondlayer/stacks/sbtc"; function onEvent(event: SbtcRegistryEvent) { if (event.topic === "withdrawal-create") { console.log(formatBtcAddress(event.recipient), satsToSbtc(event.amount)); } } ``` ### Subscribing to sBTC Events Filtering the sBTC lifecycle across Index, Streams, Webhooks, and Subgraphs is the [filters](/reference/filters) module's job: `on.sbtcDeposit`, `on.sbtcWithdrawalCreate`, and friends. Deposits and withdrawals themselves are initiated on Bitcoin and by the signer set; this extension reads state, it does not broadcast. ## Simnet transport Clarinet simnet as a client transport. Same `getContract` / public actions as HTTP, against an in-process VM. ```ts import { initSimnet } from "@stacks/clarinet-sdk"; import { createPublicClient } from "@secondlayer/stacks"; import { getContract } from "@secondlayer/stacks/actions"; import { simnet, simnetChain } from "@secondlayer/stacks/simnet"; const session = await initSimnet("./Clarinet.toml"); const client = createPublicClient({ chain: simnetChain, transport: simnet(session), }); const c = getContract({ client, address: session.getAccounts().get("deployer")!, name: "counter", abi: counterAbi, }); await c.read.getCount(); ``` `@stacks/clarinet-sdk` is an optional peer of this entry. The root SDK does not load it. No `/extended` index (except receipts of txs this transport mined). Watches throw `SimnetUnsupportedError`. Fee estimate is `NoEstimateAvailable` so wallet actions fall back to `'min'`. ## StackingDAO StackingDAO liquid staking extension. Deposit STX, receive stSTX, earn auto-compounding stacking rewards. **Mainnet only** — no testnet deployment exists. ### Setup ```typescript import { createPublicClient, createWalletClient, http, mainnet } from "@secondlayer/stacks"; import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; import { stackingDao } from "@secondlayer/stacks/stackingdao"; const client = createPublicClient({ chain: mainnet, transport: http(), }).extend(stackingDao()); const wallet = createWalletClient({ account: privateKeyToAccount("0x..."), chain: mainnet, transport: http(), }).extend(stackingDao()); ``` ### Deposit STX ```typescript // Deposit 100 STX, receive stSTX await wallet.stackingDao.deposit({ amount: 100_000_000n }); // With referrer await wallet.stackingDao.deposit({ amount: 100_000_000n, referrer: "SP2J6...", }); ``` ### Withdraw Three withdrawal paths: #### Standard Withdrawal (2-step) ```typescript // Step 1: Burn stSTX, receive NFT receipt await wallet.stackingDao.initWithdraw({ ststxAmount: 95_000_000n }); // Step 2: After unlock height, burn NFT, receive STX await wallet.stackingDao.withdraw({ nftId: 42n }); ``` #### Instant Withdrawal (idle STX only) ```typescript // Withdraw from idle reserve — instant, no NFT await wallet.stackingDao.withdrawIdle({ ststxAmount: 1_000_000n }); ``` ### Read-Only Queries ```typescript // stSTX balance const balance = await client.stackingDao.getStSTXBalance("SP2J6..."); // Exchange rate info const rate = await client.stackingDao.getExchangeRate(); console.log(rate.stxPerStstx); // STX per 1 stSTX console.log(rate.totalStx); // total STX in protocol console.log(rate.ststxSupply); // total stSTX minted // Total stSTX supply const supply = await client.stackingDao.getTotalSupply(); // Withdrawal NFT info const info = await client.stackingDao.getWithdrawalInfo(42n); if (info) { console.log(info.ststxAmount); console.log(info.stxAmount); console.log(info.unlockBurnHeight); } // Fee rates const fees = await client.stackingDao.getFees(); console.log(fees.stackFee); console.log(fees.unstackFee); console.log(fees.withdrawIdleFee); // Reserve balance const reserve = await client.stackingDao.getReserveBalance(); // Deposit shutdown status const shutdown = await client.stackingDao.getShutdownDeposits(); ``` ### Architecture Note StackingDAO's core contract functions require multiple trait arguments (reserve, commission, staking, direct-helpers). The extension fills these automatically — you only pass your data. ## Subscriptions Real-time WebSocket subscriptions for blocks, mempool, transactions, and balances. ### Setup Requires a `webSocket` transport. ```typescript import { createPublicClient, webSocket, mainnet } from "@secondlayer/stacks"; const client = createPublicClient({ chain: mainnet, transport: webSocket(), }); ``` ### Watch Blocks ```typescript const sub = await client.watchBlocks({ onBlock: (block) => { console.log("New block:", block.height); }, }); // Unsubscribe sub.unsubscribe(); ``` ### Watch Mempool ```typescript const sub = await client.watchMempool({ onTransaction: (tx) => { console.log("Pending tx:", tx.tx_id); }, }); ``` ### Watch Transaction ```typescript const sub = await client.watchTransaction({ txId: "0xabc...", onUpdate: (update) => { console.log("Status:", update.tx_status); }, }); ``` ### Watch Address Activity ```typescript // All transactions for an address const sub = await client.watchAddress({ address: "SP2J6...", onTransaction: (tx) => { console.log("Activity:", tx.tx_id); }, }); // Balance changes only const sub = await client.watchAddressBalance({ address: "SP2J6...", onChange: (balance) => { console.log("New balance:", balance.stx.balance); }, }); ``` ### Watch NFT Events ```typescript const sub = await client.watchNftEvent({ assetIdentifier: "SP2J6....my-nft::my-nft", onEvent: (event) => { console.log("NFT event:", event); }, }); ``` ## Transactions Build, sign, and serialize Stacks transactions (SIP-005). ### Token Transfer ```typescript import { buildTokenTransfer, signTransaction, serializeTransactionHex } from "@secondlayer/stacks/transactions"; const tx = buildTokenTransfer({ recipient: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", amount: 1_000_000n, memo: "coffee", fee: 200n, nonce: 0n, publicKey: "03ab...", }); const signed = signTransaction(tx, "0xprivatekey..."); const hex = serializeTransactionHex(signed); // broadcast hex to the network ``` ### Contract Call ```typescript import { buildContractCall, signTransaction } from "@secondlayer/stacks/transactions"; import { Cl } from "@secondlayer/stacks/clarity"; const tx = buildContractCall({ contractAddress: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", contractName: "my-contract", functionName: "transfer", functionArgs: [Cl.uint(100), Cl.principal("SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE")], fee: 500n, nonce: 1n, publicKey: "03ab...", }); ``` ### Contract Deploy ```typescript import { buildContractDeploy, signTransaction } from "@secondlayer/stacks/transactions"; const tx = buildContractDeploy({ contractName: "my-token", codeBody: "(define-fungible-token my-token)", fee: 10_000n, nonce: 2n, publicKey: "03ab...", }); ``` ### Multi-Sig (2-of-3) ```typescript import { buildTokenTransfer, signMultiSig } from "@secondlayer/stacks/transactions"; const tx = buildTokenTransfer({ recipient: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", amount: 1_000_000n, fee: 200n, nonce: 0n, publicKeys: [pk1, pk2, pk3], signaturesRequired: 2, }); // Sign sequentially — auto-finalizes when threshold is met const partial = signMultiSig(tx, key1, [pk1, pk2, pk3]); const full = signMultiSig(partial, key2, [pk1, pk2, pk3]); ``` #### Non-Sequential (SIP-027) Signers can sign independently and combine later. ```typescript import { buildTokenTransfer, signMultiSig, combineMultiSigSignatures } from "@secondlayer/stacks/transactions"; import { AddressHashMode } from "@secondlayer/stacks/transactions"; const tx = buildTokenTransfer({ recipient: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", amount: 1_000_000n, fee: 200n, nonce: 0n, publicKeys: [pk1, pk2, pk3], signaturesRequired: 2, hashMode: AddressHashMode.P2SH_NonSequential, }); const sig1 = signMultiSig(tx, key1, [pk1, pk2, pk3]); const sig3 = signMultiSig(tx, key3, [pk1, pk2, pk3]); const combined = combineMultiSigSignatures(tx, [sig1, sig3]); ``` ### Sponsored Transactions ```typescript import { buildTokenTransfer, signTransaction, signSponsor } from "@secondlayer/stacks/transactions"; // Origin builds with sponsored: true and fee: 0 const tx = buildTokenTransfer({ recipient: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", amount: 1_000_000n, fee: 0n, nonce: 0n, publicKey: originPubKey, sponsored: true, }); // Origin signs const originSigned = signTransaction(tx, originKey); // Sponsor sets fee and signs const fullySigned = signSponsor(originSigned, sponsorKey); ``` ### Enums #### ClarityVersion | Name | Value | | ---------- | ----- | | `Clarity1` | 1 | | `Clarity2` | 2 | | `Clarity3` | 3 | | `Clarity4` | 4 | | `Clarity5` | 5 | #### TenureChangeCause | Name | Value | | --------------------- | ----- | | `BlockFound` | 0x00 | | `Extended` | 0x01 | | `ExtendedRuntime` | 0x02 | | `ExtendedReadCount` | 0x03 | | `ExtendedReadLength` | 0x04 | | `ExtendedWriteCount` | 0x05 | | `ExtendedWriteLength` | 0x06 | ### Wire Format ```typescript import { serializeTransaction, serializeTransactionHex, deserializeTransaction } from "@secondlayer/stacks/transactions"; const bytes = serializeTransaction(signed); // Uint8Array const hex = serializeTransactionHex(signed); // string const parsed = deserializeTransaction(hex); // StacksTransaction ``` ## Utils Encoding, hashing, address, and unit conversion utilities. ### Encoding ```typescript import { bytesToHex, hexToBytes, utf8ToBytes, bytesToUtf8 } from "@secondlayer/stacks/utils"; bytesToHex(new Uint8Array([0xde, 0xad])); // "dead" hexToBytes("deadbeef"); // Uint8Array utf8ToBytes("hello"); // Uint8Array bytesToUtf8(bytes); // "hello" ``` ### Hashing ```typescript import { sha256, hash160, ripemd160 } from "@secondlayer/stacks/utils"; const h = sha256(data); // Uint8Array (32 bytes) const h160 = hash160(data); // Uint8Array (20 bytes) — SHA-256 + RIPEMD-160 ``` ### Addresses ```typescript import { c32address, c32addressDecode, publicKeyToAddress, validateStacksAddress, parseContractId, } from "@secondlayer/stacks/utils"; const addr = c32address(22, hash160Bytes); // "SP2J6..." const [version, hash] = c32addressDecode("SP2J6..."); publicKeyToAddress("02e3af14..."); // single-sig mainnet address publicKeyToAddress("02e3af14...", "testnet"); // "ST..." validateStacksAddress("SP2J6..."); // true parseContractId("SP2J6....my-contract"); // { address: "SP2J6...", name: "my-contract" } ``` ### Units ```typescript import { formatStx, parseStx, formatUnits, parseUnits } from "@secondlayer/stacks/utils"; formatStx(1_000_000n); // "1.0" parseStx("1.5"); // 1_500_000n formatUnits(1000n, 6); // "0.001" parseUnits("0.001", 6); // 1000n ``` ### Signatures ```typescript import { verifyMessageSignature, recoverPublicKey } from "@secondlayer/stacks/utils"; const valid = verifyMessageSignature({ message: "Hello", signature: "0x...", publicKey: "03ab...", }); const pubkey = recoverPublicKey(hash, signature); ``` ## Bitcoin addresses from the same mnemonic Derive the paired BTC account (what Leather/Xverse show next to your Stacks address) with no extra dependencies — BIP84 native segwit or BIP86 taproot, network-aware: ```ts import { mnemonicToBitcoinKeys } from "@secondlayer/stacks/accounts"; const btc = mnemonicToBitcoinKeys(mnemonic, { type: "p2tr" }); btc.address; // bc1p… (path m/86'/0'/0'/0/0) mnemonicToBitcoinKeys(mnemonic, { type: "p2wpkh", network: "testnet" }).address; // tb1q… ``` Pure derivation — no Bitcoin transaction building or signing. The pubkey→address helpers (`publicKeyToP2wpkhAddress`, `publicKeyToP2trAddress`, `taprootTweakPubkey`) are exported from `@secondlayer/stacks/bitcoin`, validated against the BIP84/86/341 test vectors. The sBTC extension uses the same machinery to derive the **signers' deposit address** straight from the on-chain registry — network-aware, so testnet gives `tb1p…` instead of a wrong-network address: ```ts const client = createPublicClient({ chain: mainnet, transport: http() }).extend(sbtc()); await client.sbtc.getSignersAddress(); // bc1p… (derived from get-current-aggregate-pubkey) await client.sbtc.getSignersPublicKey(); // 33-byte aggregate key ``` ## Bitcoin SPV Prove a Bitcoin payment happened inside a Stacks contract, with no oracle. :::info[On-chain at Epoch 4.0, Bitcoin block 960,230] Proof construction, Clarity codecs, and byte handling work **today** against live Bitcoin. The on-chain calls hit the SIP-044 built-ins, which exist once **Clarity 6 / Stacks Epoch 4.0** activates on mainnet. Run the round-trip locally any time in **Clarinet ≥ 3.21 simnet**, no node. ::: ### The two halves * **On-chain (the node):** `get-bitcoin-tx-output?` parses one output of a serialized BTC tx; `verify-merkle-proof` proves it committed in a block. * **Off-chain (`@secondlayer/stacks/bitcoin`):** shape the data those built-ins demand (right merkle proof, internal byte order, witness stripped) and decode the results. Unlocks: | Use case | Why native SPV | | ----------------------------- | ---------------------------------- | | BTC-settled escrow / OTC | Release on proof, no oracle | | BTC-L1 collateral | Prove the lock without bridging | | Atomic BTC ↔ sBTC/Runes swaps | Uncapped, unlike `clarity-bitcoin` | | Trust-minimized sBTC deposits | SIP-028 | | Proof-of-payment receipts | Verifiable on-chain | ### Install ```bash bun add @secondlayer/stacks ``` Subpath import; tree-shakes out if unused. ```ts import { buildTxProof, verifyBitcoinPayment } from "@secondlayer/stacks/bitcoin"; ``` ### Verify a Bitcoin payment `verifyBitcoinPayment` composes the flow: build the proof, decode the funded output, run the on-chain check, assert your expectations. ```ts import { createPublicClient, http } from "@secondlayer/stacks"; import { mainnet } from "@secondlayer/stacks/chains"; import { buildTxProof, bitcoinRpcSource, esploraSource, fallbackProofSource, verifyBitcoinPayment, } from "@secondlayer/stacks/bitcoin"; const client = createPublicClient({ chain: mainnet, transport: http() }); // Trustless by default: your own node first, hosted fallback second. const source = fallbackProofSource([ bitcoinRpcSource({ url: "http://127.0.0.1:8332", auth: { username: "u", password: "p" } }), esploraSource({ url: "https://blockstream.info/api" }), ]); // "release only when a real BTC payment to for is proven on-chain" const result = await verifyBitcoinPayment(client, { txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", source, vout: 0, expect: { address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", amount: 5_000_000_000n }, }); // → { verified, mined, output, proof } ``` `verified` is `true` when the tx is `mined` **and** every `expect` field matches. Pass `proof` instead of `txid + source` if you already have one. :::note[`contract` is optional on mainnet] Omit it and the reference adapter resolves automatically. Pass it to use your own verifier, or on any network without a published adapter, where the call throws until you do. ::: ### Build a proof `buildTxProof` re-verifies every claim a source makes: txids hash correctly, the index points at the tx, the proof folds to the header's root. A hostile source fails loudly. ```ts import { buildTxProof, esploraSource } from "@secondlayer/stacks/bitcoin"; const source = esploraSource({ url: "https://blockstream.info/api" }); const proof = await buildTxProof(source, { txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", vout: 0, }); // proof: { rawTx, txidInternal, vout, merkle: { siblings, txIndex, txCount }, header, height } ``` | Source | Use | | ----------------------------------- | ------------------------------------------------------------------------------------ | | `bitcoinRpcSource({ url, auth })` | Your own Bitcoin Core node (`-txindex`). Trustless, the default. | | `esploraSource({ url })` | Any Esplora REST endpoint: blockstream, mempool.space, self-hosted. Hosted fallback. | | `fallbackProofSource([primary, …])` | Chain them: try your node, fall back to hosted. | Also exported: `parseBitcoinTx`, `parseBlockHeader`, `stripWitness`, `doubleSha256`, `reverseBytes`, `buildMerkleProof`, `merkleRoot`. ### Encode for Clarity Internal byte order, flat args, `tx-count` (not tree depth): the foot-gun this module absorbs. ```ts import { encodeMerkleProofArgs, decodeTxOutput, parseOutputScript, } from "@secondlayer/stacks/bitcoin"; // `(leaf, root, tx-index, tx-count, (list 24 (buff 32)))` — never reversed, never a tuple. const args = encodeMerkleProofArgs({ leaf: proof.txidInternal, root, proof: proof.merkle }); // Decode `get-bitcoin-tx-output?`'s `{ script, amount, txid }` tuple, then read the script. const out = decodeTxOutput(resultCV); // { script, amount: bigint, txid } const spk = parseOutputScript(out.script); // { type: "p2pkh" | "p2wpkh" | "p2tr" | …, address?, data? } ``` ### The reference adapter Built-ins are callable only from *inside* a contract, never over RPC. The reference [`spv-adapter`](https://github.com/ryanwaits/secondlayer/blob/main/contracts/contracts/spv-adapter.clar) wraps them: read-only, no state, no custody, at `clarity_version = 6` / `epoch = "4.0"`. Copy it, or ship your own with the same shape. | Network | Adapter | | ------- | --------------------------------------------------------------------------------------- | | Mainnet | `SP2M1DE95TS0QBM4K893X6ST49FFJ53CCX9CYWNVY.spv-adapter`, live at Epoch 4.0 | | Testnet | None: testnet has no Epoch 4.0, so the built-ins don't exist. Pass your own `contract`. | | Devnet | Deploy it yourself; set `epoch_4_0` in `settings/Devnet.toml`. | `SPV_ADAPTER_CONTRACTS` and `getSpvAdapter(network)` expose the principal to pin it yourself. | Function | Wraps | | -------------------- | -------------------------------------------------------------------------------------------------- | | `get-tx-output` | `get-bitcoin-tx-output?`, parses one output | | `verify-merkle` | `verify-merkle-proof`, membership under a supplied root | | `header-merkle-root` | slice the merkle root out of an 80-byte header | | `was-tx-mined` | composed: authenticate the header against `get-burn-block-info?`, then prove inclusion, atomically | `was-tx-mined` returns: | Result | Meaning | | ------------ | -------------------------------------------- | | `(ok true)` | header canonical **and** tx included (mined) | | `(ok false)` | header canonical, tx not included | | `(err u1)` | header isn't the canonical block at `height` | | `(err u2)` | malformed header length | ```ts import { bitcoinVerifier } from "@secondlayer/stacks/bitcoin"; const verifier = bitcoinVerifier(client, { contract: "SP2M1DE95TS0QBM4K893X6ST49FFJ53CCX9CYWNVY.spv-adapter", }); const mined = await verifier.wasTxMined(proof); ``` To gate value on a proof, call the same built-ins from a `define-public` in your own contract, which is consensus-enforced rather than a read-only query. ### Run it in Clarinet simnet [Clarinet](https://github.com/hirosystems/clarinet) **≥ 3.21** boots simnet at Epoch 4.0, so the built-ins resolve locally. Register your verifier in `Clarinet.toml`: ```toml [contracts.spv-adapter] path = "contracts/spv-adapter.clar" clarity_version = 6 epoch = "4.0" ``` Drive it with [`getContract` + `simnet()`](/guide/transactions#test-against-clarinet-simnet). ### Gate on activation SIP-044 rides the same Epoch 4.0 fork as `pox-5`: Bitcoin block **960,230** on mainnet, exported as `EPOCH_4_ACTIVATION_BURN_HEIGHT_MAINNET`. `isClarity6Active` compares the node's burn height against it. ```ts import { isClarity6Active } from "@secondlayer/stacks/bitcoin"; // Mainnet client: the height is known, nothing to pass. const live = await isClarity6Active(client); // Any other network has no fixed height — supply one. const onDevnet = await isClarity6Active(devnetClient, { activationBurnHeight: 120 }); ``` ### What it proves :::note[SPV trust-minimizes verification, not custody] Both prove a Bitcoin fact to a contract; neither moves or guards funds. Out-of-range heights (before the chain launched, or newer than the node's last-processed burn block) also return `(err u1)`, so a very recent tx may need to wait. Flash blocks (Bitcoin blocks with no Stacks block) are *not* a gap: `get-burn-block-info? header-hash` is indexed by burn height. ::: See also [Verification](https://secondlayer.tools/docs/verification) for inclusion proofs on the *Stacks* side, and the [SDK](https://secondlayer.tools/docs/sdk). ## Wait for confirmation `waitForTransactionReceipt` polls until a transaction is mined (optionally N confirmations deep) and returns a normalized receipt with the decoded Clarity result. It rejects with typed errors when the tx aborts (`TransactionAbortedError`, receipt attached), drops from the mempool (`TransactionDroppedError`), or times out — and it re-reads block placement every cycle, so reorgs don't strand the wait. ```ts const txid = await client.callContract({ contract, functionName: "mint" }); const receipt = await client.waitForTransactionReceipt({ txid, confirmations: 2 }); receipt.result; // decoded ClarityValue // or in one step: const { receipt } = await sendTransaction(client, { transaction, wait: 2 }); ``` Status reads are pluggable, like nonce sources: the default reads `/extended/v1/tx` on your transport host; `indexTxSource()` reads `/v1/index/transactions` on your Secondlayer instance, which returns the chain tip in the same response, so N-confirmation waits cost one request per poll. The index only knows mined transactions, so with this source the dropped-grace window defaults to the full `timeout` instead of 30s. Without `baseUrl` the transport URL is assumed to be your instance: a Hiro transport throws up front, and a host that answers `/v1/index` with a non-JSON 404 (a bare stacks-node) throws on the first poll instead of waiting out the timeout. A `baseUrl` other than the transport host reuses the transport's retry and timeout policy only; the transport's `apiKey` and `fetchOptions` stay with the transport host. Rejection reasons are typed too: `BroadcastError.reason` is a literal union of all 26 stacks-node rejection strings (with `reasonData` and `txid` attached). ## Errors The HTTP transport throws a typed `HttpRequestError` (`.status`, `.url`, `.method` attached) on any non-2xx response instead of handing back the error body as if it were a successful result, and it retries `429`s, not just `5xx`/network errors. One `timeout` covers each attempt end to end, headers and body, so a stalled response rejects with `TimeoutError` (`.url`, `.method`, `.timeout`, `.attempt`) instead of hanging. Pass `signal` on any request to cancel from your side; a caller abort is never retried. Broadcasts are sent once (`retryCount: 0`): re-posting a transaction the node may already hold would surface as a nonce conflict, so a timed-out broadcast checks whether the node knows the tx before failing. `MalformedResponseError` throws from `getBalance`, `getAccountInfo`, `getNonce`, `getMapEntry`, `getBlockHeight`, `getBurnBlockHeight`, and `readContract` when the node's response is missing an expected field, instead of failing with an opaque native error (`BigInt(undefined)` and friends). `readContract` (and `getContract().read`) throws `ReadContractError` when the node answers `okay: false`; the message is the node's cause. Both are exported from `@secondlayer/stacks`. Every error carries a stable `code` (`HTTP_REQUEST_ERROR`, `TIMEOUT_ERROR`, `MALFORMED_RESPONSE_ERROR`, `READ_CONTRACT_ERROR`, ...) that also appears in `toJSON()`, so a handler can branch on it without an `instanceof` chain and without parsing a message that may be reworded. ```ts import { BaseError, HttpRequestError, MalformedResponseError, ReadContractError, TimeoutError } from "@secondlayer/stacks"; try { await client.getBalance({ address }); } catch (e) { if (e instanceof HttpRequestError) e.status; // non-2xx from the node/API if (e instanceof TimeoutError) e.url; // headers or body did not arrive in time if (e instanceof MalformedResponseError) { /* response shape didn't match */ } if (e instanceof ReadContractError) e.shortMessage; // call-read okay: false, node cause if (e instanceof BaseError) e.code; // "TIMEOUT_ERROR", stable across releases and minification (derived from `name`, not the class) } ``` ## Fee tiers Every send action takes `fee` as an exact amount **or a named tier**: `'min' | 'low' | 'mid' | 'high'`. Tiers map to the node's three estimations; `'min'` is the minimum relay fee (1 uSTX per serialized byte), computed offline with no round-trip. Omitting `fee` estimates mid. When the node answers `NoEstimateAvailable` (a quiet chain or fresh devnet with no fee history), the SDK falls back to `'min'` instead of failing; `resolveFee` returns `{ fee, tier }` so you can see which happened. Any other estimator failure (timeout, 5xx, auth) throws rather than silently under-paying, and the nonce reserved for that send is handed back. ```ts await client.transferStx({ to, amount: 1000n, fee: "low" }); await client.callContract({ contract, functionName: "mint", fee: "min" }); ``` ## Nonce management Stacks' `/v2/accounts` returns only the confirmed nonce — it ignores the mempool. Broadcasting several transactions from one account before the first confirms makes them reuse the same nonce, so every one after the first is rejected (`ConflictingNonceInMempool`). The usual workaround is tracking nonces by hand. Attach a nonce manager and the SDK hands out sequential nonces across rapid broadcasts: ```ts import { createWalletClient, http, createNonceManager } from "@secondlayer/stacks"; import { mainnet } from "@secondlayer/stacks/chains"; import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; const client = createWalletClient({ chain: mainnet, transport: http(), // any node — no Secondlayer dependency account: privateKeyToAccount(process.env.KEY!), nonceManager: createNonceManager(), // jsonRpcSource + in-memory store }); // 20 back-to-back transfers → nonces n, n+1, …, n+19 — no collisions await Promise.all( recipients.map((to) => client.transferStx({ to, amount: 1000n })), ); ``` Passing an explicit `nonce` always bypasses the manager. The defaults are node-agnostic and in-memory, with zero external dependencies. A send that fails before the node accepts it (fee estimate outage, `FeeTooLow`, transport error) hands its nonce back, so the next send reuses it instead of leaving a gap the mempool cannot chain past. Custom stores opt in by implementing `release(key, nonce)`; the bundled memory, Redis and Postgres stores already do. #### Multiple processes / smart wallets The in-memory store is single-process. Backends that sign from one key across multiple workers (smart-wallet-as-a-service) need a shared, durable store. The reservation is atomic in the datastore, so it doubles as the cross-process lock and survives restarts: ```ts import { createNonceManager, redisStore } from "@secondlayer/stacks"; const nonceManager = createNonceManager({ store: redisStore({ redis: new Bun.RedisClient(process.env.REDIS_URL!) }), }); ``` `postgresStore({ sql })` works the same way. Bring your own client — no global `Bun` reference, so the store stays runtime-agnostic. #### Mempool-aware sources (optional) By default the floor is the node's confirmed nonce. To make it mempool-aware, and to auto-fill the freed nonce of a dropped transaction, swap the source. `indexSource` reads your Secondlayer instance's mempool through the client transport (retries, timeout and `Authorization: Bearer` included), `hiroNonceSource` reads Hiro's, or bring your own pending feed with `mempoolAwareSource`: ```ts import { createNonceManager, indexSource, startNonceReconciler, } from "@secondlayer/stacks"; // indexSource({ baseUrl?, apiKey? }) | hiroNonceSource({ baseUrl }) | mempoolAwareSource({ getPending }) // baseUrl defaults to the transport URL; pass it when the transport is not your instance. // A missing URL, a Hiro transport, or a host without /v1/index rejects instead of // silently falling back to the confirmed nonce; a 5xx or timeout still degrades. const source = indexSource(); const nonceManager = createNonceManager({ source }); // Optional: periodically heal silently-dropped txs (run in ONE process) const reconciler = startNonceReconciler(nonceManager, { client, addresses: [account.address], source, }); ``` Everything here is opt-in. With no `source`/`store`, the manager depends only on your node. ## PoX-5 Bitcoin Staking Reading bond prints from your instance is on [PoX-5 events](https://secondlayer.tools/docs/pox5-events). This page is the wallet and proof side, from `@secondlayer/stacks`. :::info[Wallet actions gate on chain-reported activation.] `client.pox5.isActive()` reads `/v2/pox`, not a hardcoded height. Epoch 4.0 activated at Bitcoin block **960,230**. ::: ### Install ```bash bun add @secondlayer/stacks ``` Subpath module; tree-shakes out if unused. ```ts import { pox5 } from "@secondlayer/stacks/pox5"; ``` ### Quick start ```ts import { createWalletClient, http, mainnet } from "@secondlayer/stacks"; import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; import { pox5 } from "@secondlayer/stacks/pox5"; const client = createWalletClient({ account: privateKeyToAccount(process.env.KEY!), chain: mainnet, transport: http(), }).extend(pox5()); if (await client.pox5.isActive()) { const txid = await client.pox5.stake({ signerManager: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7.signer-mgr", amountUstx: 100_000_000_000n, // 100,000 STX numCycles: 12, startBurnHeight: 960_231, fee: "low", }); await client.waitForTransactionReceipt({ txid, confirmations: 1 }); } ``` Every action routes through the same pipeline as `callContract`: fee tiers (`min` | `low` | `mid` | `high`), managed nonces, typed `BroadcastError` reasons. ### Gate on activation `isActive()` and `getActivation()` read `/v2/pox` `contract_versions`: the node reports where `pox-5` activates on *its* network, so one code path is correct everywhere. ```ts const activation = await client.pox5.getActivation(); // { contractId, activationBurnchainBlockHeight, firstRewardCycleId } // undefined until the node runs stacks-core >= 4.0.0 const live = await client.pox5.isActive(); // true once the burnchain reaches the activation height ``` Actions against an inactive chain fail with a descriptive error naming the activation height, never a raw contract abort. ### Read a staker's position `getStakerState` returns a staker's whole position in **one** batched request. ```ts const state = await client.pox5.getStakerState("SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7"); // { stakerInfo, bondMembership, custodiedSbtc, currentCycle } ``` Individual reads are exposed too, and an optional read returns `null` when the record doesn't exist: `getStakerInfo`, `getBondMembership`, `getProtocolBond`, `getBondAllowance`, `getTotalSbtcStakedForBond`, `getStakerCustodiedSbtc`, `hasAnnouncedL1EarlyExit`, `getBondL1UnlockHeight`, `getSignerInfo`, `verifySignerKeyGrant`, `getCurrentRewardCycle`, `getFirstRewardCycle`. ```ts const info = await client.pox5.getStakerInfo("SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7"); // { amountUstx, firstRewardCycle, numCycles, signer } | null ``` ### Wallet actions Every `pox-5` public function, typed. All accept `fee`, `nonce`, `postConditions`, `postConditionMode`; all return a txid. | Action | Contract call | Does | | ----------------------------- | --------------------------------- | --------------------------------------------------------------------------------- | | `setupBond` | `setup-bond` | Configure a bond's rate, ratios, early-unlock script, allowlist (bond-admin only) | | `registerForBond` | `register-for-bond` | Join a bond with custodied sBTC **or** proven L1 BTC lockups | | `updateBondRegistration` | `update-bond-registration` | Switch signer-managers mid-bond | | `stake` | `stake` | STX-only staking | | `stakeUpdate` | `stake-update` | Extend and/or increase an STX-only position | | `unstake` | `unstake` | Wind down an STX-only position at the next cycle | | `unstakeSbtc` | `unstake-sbtc` | Withdraw custodied sBTC (rejected in prepare phase) | | `announceL1EarlyExit` | `announce-l1-early-exit` | Signal an L1 early exit (rejected in prepare phase) | | `calculateRewards` | `calculate-rewards` | Settle reward accounting for up to 6 bond periods | | `claimRewards` | `claim-rewards` | Signer-manager claims a cycle's accrued rewards | | `claimStakerRewardsForSigner` | `claim-staker-rewards-for-signer` | Claim a staker's rewards via their signer | | `grantSignerKey` | `grant-signer-key` | Register a signed signer-key grant | | `revokeSignerGrant` | `revoke-signer-grant` | Revoke a grant | `registerForBond` takes the BTC side as a union: custodied sBTC, or SPV-proven L1 lockup outputs: ```ts // sBTC path await client.pox5.registerForBond({ bondIndex: 0, signerManager: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7.signer-mgr", amountUstx: 500_000_000_000n, btcLockup: { sbtcSats: 100_000_000n }, }); // Proven L1 lockup path — proof fields come from buildTxProof btcLockup: { l1Outputs: [{ height: proof.height, tx: proof.rawTx, outputIndex: proof.vout, header: proof.header, leafHashes: proof.merkle.siblings, txCount: proof.merkle.txCount, txIndex: proof.merkle.txIndex, amount: 100_000_000n, unlockBurnHeight: 987_530n, }], stakerUnlockBytes, } ``` Proof fields come from `buildTxProof` in [`@secondlayer/stacks/bitcoin`](/guide/bitcoin-spv); the contract verifies the lockup tx's Bitcoin inclusion on-chain. ### L1 BTC lockup scripts Byte-for-byte TypeScript ports of the contract's script constructors, and the contract validates your L1 output against *exactly* these bytes. ```ts import { buildDefaultStakerUnlockBytes, buildLockupAddress, buildLockupScript, stakerPreimage, } from "@secondlayer/stacks/pox5"; const stakerUnlockBytes = buildDefaultStakerUnlockBytes(compressedPubkey); // OP_CHECKSIG const address = buildLockupAddress( { stxAddress: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", unlockBurnHeight: 987_530, stakerUnlockBytes, earlyUnlockBytes, // from the bond's protocol-bonds.early-unlock-bytes }, "mainnet", ); // bc1q… — send the BTC lockup here ``` * `buildLockupScript`: the witness script, a CLTV branch (spendable at `unlockBurnHeight`) and an early-exit branch that must reveal `stakerPreimage(stxAddress)`. * `buildLockupOutputScript`: the P2WSH `scriptPubKey` (`0x0020 || sha256(script)`). * `buildLockupAddress`: the bech32 P2WSH address; network-aware, including `regtest`. * `buildDefaultStakerUnlockBytes`: the common single-key staker subscript; any script tail is valid. * `stakerPreimage`: the 32-byte witness item the early-exit branch reveals. Output is byte-compared against the contract's own `construct-lockup-script` in CI. ### Signer-key grants SIP-018 structured-data signatures authorizing a signer-manager contract to use a signer key. ```ts import { computeSignerGrantHash, signSignerGrant, verifySignerGrant, } from "@secondlayer/stacks/pox5"; const opts = { signerManager: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7.signer-mgr", authId: 1n, chainId: mainnet.id, }; const signerSig = await signSignerGrant(signerAccount, opts); // 65-byte RSV hex verifySignerGrant({ ...opts, publicKey: signerPubkey, signature: signerSig }); // true ``` `signSignerGrant` returns **RSV** order, exactly the `(buff 65)` layout `grant-signer-key` expects. `computeSignerGrantHash` is byte-identical to the contract's `get-signer-grant-message-hash`. ### Cycle math Pure functions mirroring the contract's cycle read-onlys. Pass chain-reported parameters from `/v2/pox` and `getActivation()`. ```ts import { burnHeightToRewardCycle, bondPhaseAtHeight, isInPreparePhase } from "@secondlayer/stacks/pox5"; const params = { firstBurnchainBlockHeight: 666_050, rewardCycleLength: 2_100 }; // from /v2/pox const firstBondPeriodCycle = activation!.firstRewardCycleId; // from getActivation() burnHeightToRewardCycle(960_231, params); // → cycle number bondPhaseAtHeight(0, 960_231, { ...params, firstBondPeriodCycle }); // "too-early" | "open" | "locked" | "unlocked" isInPreparePhase(960_231, { ...params, prepareCycleLength: 100 }); // boolean ``` Also exported: `rewardCycleToBurnHeight`, `bondPeriodToRewardCycle`, `bondPeriodToBurnHeight`, `bondUnlockCycle`, `burnHeightToDistributionIndex`. :::warning[Prepare phase rejects exits] `unstake-sbtc` and `announce-l1-early-exit` are rejected during a cycle's prepare phase (the final `prepare_cycle_length` blocks). Check `isInPreparePhase` before broadcasting, or your exit transaction burns a fee to abort. ::: ### Post-conditions Pass Epoch 4.0 `staking-postcondition` / `pox-postcondition` types via `postConditions` on any action: ```ts await client.pox5.stake({ signerManager: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7.signer-mgr", amountUstx: 100_000_000_000n, numCycles: 12, startBurnHeight: 960_231, postConditions: [ { type: "staking-postcondition", address: "origin", condition: "lte", amount: 100_000_000_000n }, { type: "pox-postcondition", address: "origin", condition: "will-not-perform" }, ], }); ``` Semantics: [SIP-045 staking post-conditions](/guide/transactions#sip-045-staking-post-conditions). See also the [Stacks SDK](/guide/transactions) and [Bitcoin SPV](/guide/bitcoin-spv). ## Clarinet simnet Same client as HTTP, against an in-process VM (`@stacks/clarinet-sdk` is an optional peer of this entry). ```ts import { initSimnet } from "@stacks/clarinet-sdk"; import { createPublicClient } from "@secondlayer/stacks"; import { simnet, simnetChain } from "@secondlayer/stacks/simnet"; const session = await initSimnet("./Clarinet.toml"); const client = createPublicClient({ chain: simnetChain, transport: simnet(session), }); ``` Then `getContract` as on mainnet. No `/extended`; watches throw; fees use `'min'`. ## Stacks SDK Build, sign, and broadcast Stacks transactions from TypeScript. Where [`@secondlayer/sdk`](https://secondlayer.tools/docs/sdk) reads decoded chain data, `@secondlayer/stacks` writes to the chain. ### Install ```bash bun add @secondlayer/stacks ``` ### Call a contract ```ts import { createWalletClient, http, mainnet } from "@secondlayer/stacks"; import { privateKeyToAccount } from "@secondlayer/stacks/accounts"; import { Cl } from "@secondlayer/stacks/clarity"; const client = createWalletClient({ account: privateKeyToAccount(process.env.KEY!), chain: mainnet, transport: http(), }); const txid = await client.callContract({ contractAddress: "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9", contractName: "usda-token", functionName: "transfer", functionArgs: [ Cl.uint(1_000_000), Cl.principal(client.account.address), Cl.principal("SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7"), Cl.none(), ], }); ``` Fees estimated, nonces managed. ### Test against Clarinet simnet Same `getContract` client against an in-process Clarinet VM (`@stacks/clarinet-sdk` is an optional peer of `@secondlayer/stacks/simnet`). ```ts import { initSimnet } from "@stacks/clarinet-sdk"; import { createPublicClient } from "@secondlayer/stacks"; import { getContract } from "@secondlayer/stacks/actions"; import { SIP010_ABI } from "@secondlayer/stacks/clarity"; import { simnet, simnetChain } from "@secondlayer/stacks/simnet"; const session = await initSimnet("./Clarinet.toml"); const client = createPublicClient({ chain: simnetChain, transport: simnet(session), }); const deployer = session.getAccounts().get("deployer")!; const token = getContract({ client, address: deployer, name: "usda-token", abi: SIP010_ABI, }); await token.read.getBalance({ account: deployer }); ``` | Limit | Behavior | | ----------- | ------------------------------------------------- | | `/extended` | not served | | watches | throw `SimnetUnsupportedError` | | fees | `NoEstimateAvailable`; wallet actions use `'min'` | ### Post-conditions On-chain assertions the network enforces: move more than you allowed and the transaction aborts instead of settling. Attach `postConditions` to any `callContract`, `transferStx`, or `deployContract` call (mode defaults to `deny`). ```ts import { Pc } from "@secondlayer/stacks/postconditions"; const txid = await client.callContract({ // …call as above postConditions: [ Pc.origin() .willSendLte(1_000_000) .ft("SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.usda-token", "usda"), ], }); ``` Or pass plain objects, the same shape stacks.js uses, portable between SDKs: ```ts postConditions: [ { type: "stx-postcondition", address: "origin", condition: "lte", amount: 1_000_000, }, ] ``` | Type | Protects | Condition | | ----------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | `stx-postcondition` | STX movement | `eq`, `gt`, `gte`, `lt`, `lte` + amount | | `ft-postcondition` | SIP-010 token movement | same, scoped to `asset` | | `nft-postcondition` | SIP-009 asset movement | `sent`, `not-sent` + `assetId` | | `staking-postcondition` | How much STX a tx may stake or re-parameterize (SIP-045): `stake`, `register-for-bond`, `stake-update` | `eq`–`lte` + amount | | `pox-postcondition` | Non-locking PoX state changes (SIP-045): `unstake`, `announce-l1-early-exit`, bond-registration updates | `will-not-perform`, `may-perform`, `will-perform` | #### SIP-045 staking post-conditions Bound a `pox-5` call from both sides: ```ts const txid = await client.callContract({ contractAddress: "SP000000000000000000002Q6VF78", contractName: "pox-5", functionName: "register-for-bond", functionArgs: [/* … */], postConditions: [ { type: "staking-postcondition", address: "origin", condition: "lte", amount: 500_000_000_000n, }, { type: "pox-postcondition", address: "origin", condition: "will-not-perform", }, ], }); ``` :::note[Activates at Epoch 4.0] Wire format ships in `@secondlayer/stacks@2.10.0`; the network accepts these only after SIP-045 activates. ::: Decoding is strict: deserializing a transaction with an unknown post-condition type throws instead of silently misreading the bytes after it. Your indexer sees an error, never corrupt data. The `pox-5` calls these guard have their own typed surface; see [PoX-5 Bitcoin Staking](/guide/pox5). ### Also in the box * **Typed contracts**: `getContract()` binds an ABI to a client for typed reads (`read.*`), broadcasts (`call.*`), unsigned wallet-signs-later transactions (`buildCall.*`), and map lookups (`maps.*`); generate branded ABIs with [`secondlayer codegen contracts`](https://secondlayer.tools/docs/cli#generate-typed-contract-clients) for named-alias hovers. * **Clarity values**: `Cl.*` constructors and decoding via `@secondlayer/stacks/clarity`. * **Bitcoin SPV**: merkle proofs and Bitcoin payment verification, see [Bitcoin SPV](/guide/bitcoin-spv). * **PoX-5 staking**: SIP-045 bonds, staking, L1 lockup scripts, signer grants, see [PoX-5 Bitcoin Staking](/guide/pox5). * **Accounts**: `privateKeyToAccount`, `mnemonicToAccount`, WalletConnect providers via `@secondlayer/stacks/accounts` and `@secondlayer/stacks/connect`. * **Multi-sig**: `createMultiSigClient` for m-of-n signing flows. ## WalletConnect v2 Native WC v2 implementation — X25519 ECDH, AES-256-GCM envelope encryption, Ed25519 JWT relay auth. Zero cost if you don't import it, tree-shakes completely. ```ts import { connect, setProvider } from "@secondlayer/stacks/connect"; import { WalletConnectProvider, showModal } from "@secondlayer/stacks/connect/walletconnect"; const wc = new WalletConnectProvider({ projectId: "your-reown-project-id", // from cloud.reown.com metadata: { name: "My App", description: "...", url: "https://myapp.com", icons: [] }, }); // Restore existing session or pair new one if (!wc.restore()) { const { uri, approval } = await wc.pair(); showModal({ wcUri: uri, onClose: () => {} }); await approval; } setProvider(wc); const { addresses } = await connect(); ``` The built-in modal shows browser extension wallets alongside the WC QR code — users pick whichever they prefer.