A native Swift implementation of the Stacks.js core, for building Stacks wallets and apps on Apple platforms and Android: keys and accounts, transactions (single-sig, multi-sig, sponsored), Clarity values, tokens and sBTC, message signing, pox-5 staking, and Hiro's v3 API — with no JavaScript anywhere.
Every serialization path is verified against golden vectors generated by Stacks.js 7.6.0, including byte-identical RFC 6979 deterministic signatures.
| Stacks.js package | StacksKit | Notes |
|---|---|---|
@stacks/transactions |
✅ | STX transfers, contract calls, deploys (Clarity 1–6; Clarity 4 by default, as in Stacks.js), coinbase / tenure-change / Nakamoto coinbase decoding; post conditions including SIP-044 staking and PoX conditions, NFT maybe-sent, and originator mode; sponsored transactions with p2pkh or p2wpkh sponsors; multi-sig signing — sequential and SIP-027 non-sequential, resumable across signers; the Clarity value codec with Cl.parse / Cl.prettyPrint equivalents; ABI validation (validateWithAbi); verification of every signature |
@stacks/wallet-sdk |
✅ | BIP-39 secret keys, accounts at m/44'/5757'/0'/0/n, legacy m/888' identity keys, xprv / xpub import and export, watch-only derivation, config / data / app keys, Gaia addresses |
@stacks/network |
✅ | Mainnet, testnet, devnet / mocknet, custom nodes, peer network IDs, boot contracts, API keys that only travel to Hiro's hosts |
@stacks/api / node RPC |
✅ | Hiro's v3 endpoints: nonces with mempool gaps, STX / FT / NFT balances, sBTC, activity, mempool, transaction status with results, staking summaries, bond positions, the signer directory, cycles. The node: accounts, PoX info, fee tiers with Stacks.js's fallback, broadcast with rejection details, read-only calls, map entries, data variables, contract interfaces |
@stacks/encryption |
Partial | Message signing (current and legacy prefixes) and SIP-018 structured data, SHA-512/256, RIPEMD-160, HMAC, PBKDF2, recoverable signatures. ECIES, which only Gaia storage uses, is out of scope |
@stacks/stacking |
✅ | pox-4 delegate-stx family (reads and builders; mainnet retired delegation with pox-5), PoX reward addresses (base58check, bech32 / bech32m, regtest) |
@stacks/bitcoin-staking |
✅ | pox-5 stake / stake-update / unstake, bond registration, sBTC unstaking, L1 early exit, reward calculation and claims, signer-key grants and signer calldata, admin calls, every read-only, Pox5ErrorCode, cycle and bond schedule math, and the fetchEligible… dry runs |
| SIP-010, SIP-009, sBTC | ✅ | Token transfers guarded by exact post conditions; exact decimal amounts (microStxToStx / stxToMicroStx without floating point); token names, symbols, decimals, and NFT metadata from Hiro's metadata API; sBTC withdrawals (peg-out); Emily, the bridge's API: limits, withdrawal and deposit tracking, deposit notifications |
| BNS | ✅ reads | Names resolve through the BNS-V2 contract, expiry included, and addresses to their primary names. Hiro's /v1/names index stopped at the September 2024 migration, so it can name a previous owner — those reads are deprecated |
@stacks/auth, @stacks/profile, @stacks/storage, BNS writes, @stacks/cli |
❌ | Legacy web-app concepts, out of scope |
| Product | What it is | Links |
|---|---|---|
StacksKit |
Everything: the core plus the Hiro API services and the make… builders that fetch fees and nonces. What Apple apps import. |
StacksKitCore, NetworkingKit |
StacksKitCore |
Keys, addresses, Clarity, transactions, signing, and stacking arguments — no networking. Written against FoundationEssentials, so an Android binary that links only the core ships neither ICU nor FoundationNetworking. |
swift-secp256k1, swift-crypto (non-Apple) |
import StacksKit re-exports the core, so nothing changes for existing callers.
- Swift 6.2, iOS 26 / macOS 26 (matches NetworkingKit's platform floor)
- NetworkingKit 2.2.0+ for all networking (
StacksKitonly) - swift-secp256k1 (
P256K) for curve operations - swift-crypto on platforms without CryptoKit — linked only for Android, Linux, and Windows
StacksKitCore cross-compiles with the Swift SDK for Android
(swift build --swift-sdk aarch64-unknown-linux-android28 --product StacksKitCore).
Every core file imports FoundationEssentials where it exists and Foundation
elsewhere, hashing switches from CryptoKit to swift-crypto, and BIP-39's NFKD
normalization is StacksKit's own (UnicodeNormalization, table generated by
Scripts/generate-nfkd-table.swift and checked against Foundation in the test
suite) because FoundationEssentials has no normalization API.
Two things outside this package still matter for a small binary, and Perch for
Android's build-android.sh handles both: Swift autolinks the whole Foundation
stack for any dependency that merely writes import Foundation, so the build
passes -disable-autolink-library for the Foundation modules plus
--no-undefined (a real use of Foundation beyond Essentials then fails to link
instead of crashing at load time); and swift-secp256k1 0.23 spells
Int32.boolValue with NSNumber, which a one-line patch replaces until it is
fixed upstream. See perch/android/PerchStacks in the Perch repository.
The core target enables MemberImportVisibility, so a core file can't bind to
a Foundation member that a dependency's import Foundation makes visible —
the compiler rejects it here instead of the Android link failing later.
import StacksKitCore
let key = try StacksPrivateKey(hex: "…")
let transaction = try StacksTransaction(
payload: .contractCall(
contractAddress: try StacksAddress("SP000000000000000000002Q6VF78"),
contractName: "pox-5",
functionName: "stake",
functionArgs: StackingArguments.stake(
signerManager: try Principal("SP8HK160YD5GHXP69VGA0TC7AQJ1X4CDW3XVERSE.xverse-signer-manager-3"),
amountMicroStx: 500_000_000_000,
numCycles: StackingArguments.maxStakeCycles,
startBurnHeight: 963_373
)
),
publicKey: key.publicKey(),
network: .mainnet,
fee: 3_000,
nonce: 7
).signed(with: key)
let hex = try transaction.serialize() // POST to /v2/transactions yourselfSponsored transactions round out the same way: build with sponsored: true,
sign as the origin, then sponsored(by: sponsorKey, fee:nonce:).
import StacksKit
// 24-word secret key, compatible with Xverse and Leather
let mnemonic = try generateSecretKey()
let wallet = try StacksWallet(mnemonic: mnemonic)
let account = try wallet.account(at: 0)
let address = try account.address(on: .mainnet)let transaction = try await makeSTXTokenTransfer(
recipient: Principal("SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159"),
amount: 1_000_000, // microSTX
senderKey: account.privateKey,
network: .mainnet,
memo: "thanks"
)
// fee and nonce are fetched automatically when omitted; pass both to build offline
let txid = try await StacksAPIService(network: .mainnet).broadcast(transaction)let transaction = try await makeContractCall(
contractAddress: StacksAddress("SP2C2YFP12AJZB4MABJBAJ55XECVS7E4PMMZ89YZR"),
contractName: "amm-pool-v2-01",
functionName: "swap",
functionArgs: [.uint(100_000), .principal(pool), .none],
senderKey: account.privateKey,
network: .mainnet,
postConditions: [
.stx(principal: .origin, condition: .lessEqual, amount: 100_000),
]
)let stacking = StackingClient(network: .mainnet)
let info = try await stacking.poxInfo() // reads the live pox contract id
guard let burnHeight = info.currentBurnchainBlockHeight,
!info.isInPreparePhase(atBurnHeight: burnHeight) else { return } // stake reverts in the prepare phase
// Bind STX to a signer-manager (the pool). The STX locks at your own address;
// rewards are paid in sBTC on the operator's terms.
let stake = try await stacking.stake(
signerManager: Principal("SP8HK160YD5GHXP69VGA0TC7AQJ1X4CDW3XVERSE.xverse-signer-manager-3"),
amountMicroStx: 500_000_000_000,
numCycles: StackingClient.maxStakeCycles, // 96 = earn until you unstake
startBurnHeight: burnHeight,
senderKey: account.privateKey
)
let txid = try await stacking.broadcast(stake)
// Later: top up, or end the stake at the cycle boundary.
if let position = try await stacking.pox5StakerInfo(for: address) {
_ = try await stacking.stakeUpdate(signerManager: position.signer, oldSignerManager: position.signer,
amountIncreaseMicroStx: 100_000_000, senderKey: account.privateKey)
_ = try await stacking.unstake(oldSignerManager: position.signer, senderKey: account.privateKey)
}The pox-4 delegate-stx family is still available for legacy reads and builders, but mainnet retired delegation with pox-5.
let principals = PrincipalService(network: .mainnet)
let stx = try await principals.stxBalance(address) // total, spendable, locked, pending
let tokens = try await principals.fungibleTokenBalances(address)
let sats = try await principals.sbtcBalance(address)
let activity = try await principals.transactions(address, limit: 20) // pass nextCursor for more
let api = StacksAPIService(network: .mainnet)
let status = try await api.transactionStatus(txid) // pending, success, aborted, dropped
let result = try await api.callReadOnly(
contract: try info.contract(), // whatever /v2/pox reports — never hardcode pox-N
functionName: "get-staker-info",
functionArgs: [.principal(try Principal(address))]
)The v3 GETs are cached at Hiro's edge, so they cost far less of the rate limit
than read-only calls. Hiro API keys belong on servers: withAPIKey(_:) adds
x-api-key to requests bound for Hiro's API hosts — never a custom node — for
a backend such as a sponsor service. Don't ship one in an app.
let transfer = try await makeFungibleTokenTransfer(
token: Sbtc.mainnet.token,
amount: 25_000, // sats; the post condition pins exactly this
recipient: friend,
senderKey: account.privateKey
)
// Withdraw sBTC to Bitcoin. Check the bridge's limits first.
let limits = try await EmilyService().limits()
if let problem = limits.withdrawalProblem(sats: 100_000) { throw WithdrawalError(problem) }
let withdrawal = try await makeSbtcWithdrawal(
amountSats: 100_000,
recipient: PoxAddress(bitcoinAddress: "bc1q…", network: .mainnet),
maxFeeSats: 3_000,
senderKey: account.privateKey
)
// Then follow it: EmilyService().withdrawals(sender: …) reports pending → accepted → confirmed.makeNonFungibleTokenTransfer does the same for SIP-009 NFTs.
Amounts convert exactly — no floating point — and any token's display details come from Hiro's metadata API:
let microStx = DecimalUnits.stx.baseUnits(fromDecimal: "12.5") // 12_500_000
let text = DecimalUnits.sbtc.decimal(fromBaseUnits: 150_000_000) // "1.5"
let metadata = try await TokenMetadataService(network: .mainnet).fungibleToken(contract)
let display = metadata?.units?.decimal(fromBaseUnits: holding.balance) // with metadata?.symbollet bns = BNSService(network: .mainnet)
let recipient = try await bns.resolveAddress(of: "muneeb.btc") // the BNS-V2 owner; throws for expired names
let display = try await bns.primaryName(of: Principal(address)) // "muneeb.btc", or nillet nonces = try await api.nonces(for: address)
if nonces.hasNonceGap { /* nonces.missingNonces are stalling everything after them */ }
// Same transaction and nonce, a higher fee, signed again — the node drops the original.
let faster = try stuck.replacingFee(stuck.auth.fee * 2, signedWith: account.privateKey)let signature = try account.privateKey.signMessage("Sign in to Perch")
let rsv = signature.rsvHex // what Stacks.js signMessageHashRsv returns
let isValid = try account.publicKey().verifyMessage("Sign in to Perch", signature: signature)
// SIP-018 structured data, as apps request through stx_signStructuredMessage
let domain = StructuredDataDomain(name: "Perch", version: "1.0.0", network: .mainnet)
let signed = try account.privateKey.signStructuredData(message: .stringAscii("Hello"), domain: domain)Signature recovery checks r and s before handing them to libsecp256k1,
whose recovery API traps on some malformed input — a hostile signature throws
instead of crashing an app or a sponsor service.
let xpub = try HDKey(extendedKey: wallet.extendedRootKey)
.derived(path: "m/44'/5757'/0'/0")
.extendedPublicKey()
.extendedKey() // share this; it can't spend
let watched = try ExtendedPublicKey(extendedKey: xpub).derived(at: 3).stacksPublicKey.address(on: .mainnet)let policy = try MultiSigPolicy(publicKeys: [alice, bob, carol], signaturesRequired: 2, hashMode: .p2shNonSequential)
let unsigned = try await makeUnsignedSTXTokenTransfer(recipient: friend, amount: 1_000_000, multiSig: policy)
// Sign with the keys at hand; or pass the partly signed transaction along and
// let the next signer resume it with TransactionSigner(transaction:).
let signed = try unsigned.signed(with: [aliceKey, carolKey], policy: policy)let reasons = try await stacking.stakeEligibility(
staker: address,
signerManager: manager,
amountMicroStx: 500_000_000_000,
numCycles: StackingClient.maxStakeCycles
)
// [] — or, say, [.signerKeyGrantNotFound, .insufficientStx], each with a descriptionswift test runs 254 tests — 187 in StacksKitCoreTests for the core and 67
in StacksKitTests for the services and networked builders — covering:
- NIST vectors for SHA-512/256 and the reference vectors for RIPEMD-160
- BIP-39 (Trezor vector), BIP-32 test vectors 1 and 2 byte for byte, malformed extended keys, watch-only xpub derivation, and the
@stacks/wallet-sdkderivation vectors - c32 / c32check address vectors from the Stacks.js
keystests - Byte-for-byte transaction golden vectors from the Stacks.js
buildertests: transfers, contract calls, sponsored transfers including the placeholder sponsor condition, and legacy 2-of-3 multi-sig; the stacks-core SIP-027 non-sequential vector - Vectors generated with
@stacks/transactions7.6.0, signatures included: SIP-010 and SIP-009 transfers, an sBTC withdrawal, a Clarity 4 deploy, a p2wpkh sponsor, originator-mode and SIP-044 post conditions, a non-sequential multi-sig contract call, and every pox-5 builder from@stacks/bitcoin-staking - Message-signing and SIP-018 vectors, and hostile signatures (zero or overflowing scalars,
roff the curve, a point at infinity) that must throw rather than trap - Clarity: wire-format vectors and round-trips for every value shape, hostile nesting, Stacks.js's pretty-printer and parser cases, and contract-call validation against Stacks.js's test ABI
- pox-5 error names against Stacks.js and the contract, and cycle and bond schedule math against the contract's read-onlys
- Bitcoin addresses (BIP-173 / BIP-350 vectors) for PoX reward addresses, regtest, and output scripts
- Exact decimal conversion at both ends of
UInt128, and refusal of anything but a plain decimal - BNS-V2 name rules from the contract, and a captured mainnet resolution
- The services against scripted responses shaped by Hiro's published v3 schema and Emily's OpenAPI spec, plus captured mainnet
/v2/poxand Emily limits: the v3 nonce path and its/v2/accountsfallback, the fee fallback only onNoEstimateAvailable, API-key host rules, Emily's index formats, token metadata, BNS-V2 resolution and expiry, and the pox-5 eligibility checks - NFKD normalization: fixed vectors on every platform, and on Apple platforms every assigned Unicode scalar plus randomized mark sequences compared against Foundation