Skip to content
SparrowTekPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

11 Commits

Folders and files

Repository files navigation

StacksKit

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.

Coverage

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

Products

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.

Requirements

  • Swift 6.2, iOS 26 / macOS 26 (matches NetworkingKit's platform floor)
  • NetworkingKit 2.2.0+ for all networking (StacksKit only)
  • swift-secp256k1 (P256K) for curve operations
  • swift-crypto on platforms without CryptoKit — linked only for Android, Linux, and Windows

Android

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.

Usage

Offline, with the core alone

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 yourself

Sponsored transactions round out the same way: build with sponsored: true, sign as the origin, then sponsored(by: sponsorKey, fee:nonce:).

Wallet

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)

Send STX

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)

Call a contract

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),
    ]
)

Stake STX, earn Bitcoin (pox-5)

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.

Read on-chain state

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.

Tokens and sBTC

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?.symbol

Resolve names

let 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 nil

Unstick a transaction

let 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)

Sign messages

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.

Watch-only accounts

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)

Multi-sig

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)

Check a stake before paying for it

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 description

Verification

swift 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-sdk derivation vectors
  • c32 / c32check address vectors from the Stacks.js keys tests
  • Byte-for-byte transaction golden vectors from the Stacks.js builder tests: 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/transactions 7.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, r off 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/pox and Emily limits: the v3 nonce path and its /v2/accounts fallback, the fee fallback only on NoEstimateAvailable, 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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages