The Arkade SDK is a TypeScript library for building Bitcoin wallets with support for both on-chain and off-chain transactions via the Ark protocol.
npm install @arkade-os/sdk
import { SingleKey, Wallet } from '@arkade-os/sdk'
// Create a new in-memory key (or use an external signer)
const identity = SingleKey.fromHex('your_private_key_hex')
// Create a wallet with Ark support
const wallet = await Wallet.create({
identity,
// Esplora API, can be left empty - mempool.space API will be used
esploraUrl: 'https://mutinynet.com/api',
arkServerUrl: 'https://mutinynet.arkade.sh',
// Optional: specify storage adapter (defaults to InMemoryStorageAdapter)
// storage: new LocalStorageAdapter() // for browser persistence
})
import { waitForIncomingFunds } from '@arkade-os/sdk'
// Get wallet addresses
const arkAddress = await wallet.getAddress()
const boardingAddress = await wallet.getBoardingAddress()
console.log('Ark Address:', arkAddress)
console.log('Boarding Address:', boardingAddress)
const incomingFunds = await waitForIncomingFunds(wallet)
if (incomingFunds.type === "vtxo") {
// Virtual coins received
console.log("VTXOs: ", incomingFunds.vtxos)
} else if (incomingFunds.type === "utxo") {
// Boarding coins received
console.log("UTXOs: ", incomingFunds.coins)
}
Onboarding allows you to swap on-chain funds into VTXOs:
import { Ramps } from '@arkade-os/sdk'
const onboardTxid = await new Ramps(wallet).onboard();
// Get detailed balance information
const balance = await wallet.getBalance()
console.log('Total Balance:', balance.total)
console.log('Boarding Total:', balance.boarding.total)
console.log('Offchain Available:', balance.available)
console.log('Offchain Settled:', balance.settled)
console.log('Offchain Preconfirmed:', balance.preconfirmed)
console.log('Recoverable:', balance.recoverable)
// Get virtual UTXOs (off-chain)
const virtualCoins = await wallet.getVtxos()
// Get boarding UTXOs
const boardingUtxos = await wallet.getBoardingUtxos()
// Send bitcoin via Ark
const txid = await wallet.sendBitcoin({
address: 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx',
amount: 50000, // in satoshis
feeRate: 1 // optional, in sats/vbyte
})
This can be used to move preconfirmed balances into finalized balances and to manually convert UTXOs and VTXOs.
// For settling transactions
const settleTxid = await wallet.settle({
inputs, // from getVtxos() or getBoardingUtxos()
outputs: [{
address: destinationAddress,
amount: BigInt(amount)
}]
})
VTXOs have an expiration time (batch expiry). The SDK provides the VtxoManager
class to handle both:
- Renewal: Renew VTXOs before they expire to maintain unilateral control of the funds.
- Recovery: Reclaim swept or expired VTXOs back to the wallet in case renewal window was missed.
import { VtxoManager } from '@arkade-os/sdk'
// Create manager with optional renewal configuration
const manager = new VtxoManager(wallet, {
enabled: true, // Enable expiration monitoring
thresholdPercentage: 10 // Alert when 10% of lifetime remains (default)
})
Renew VTXOs before they expire to keep your liquidity accessible. This settles all VTXOs (including recoverable ones) back to your wallet with a fresh expiration time.
// Check which VTXOs are expiring soon
const expiringVtxos = await manager.getExpiringVtxos()
if (expiringVtxos.length > 0) {
console.log(`${expiringVtxos.length} VTXOs expiring soon`)
expiringVtxos.forEach(vtxo => {
const timeLeft = vtxo.virtualStatus.batchExpiry! - Date.now()
const hoursLeft = Math.floor(timeLeft / (1000 * 60 * 60))
console.log(`VTXO ${vtxo.txid} expires in ${hoursLeft} hours`)
})
// Renew all VTXOs to prevent expiration
const txid = await manager.renewVtxos()
console.log('Renewed:', txid)
}
// Override threshold percentage (e.g., renew when 5% of time remains)
const urgentlyExpiring = await manager.getExpiringVtxos(5)
Recover VTXOs that have been swept by the server or consolidate small amounts (subdust).
// Check what's recoverable
const balance = await manager.getRecoverableBalance()
console.log(`Recoverable: ${balance.recoverable} sats`)
console.log(`Subdust: ${balance.subdust} sats`)
console.log(`Subdust included: ${balance.includesSubdust}`)
console.log(`VTXO count: ${balance.vtxoCount}`)
if (balance.recoverable > 0n) {
// Recover swept VTXOs and preconfirmed subdust
const txid = await manager.recoverVtxos((event) => {
console.log('Settlement event:', event.type)
})
console.log('Recovered:', txid)
}
// Get transaction history
const history = await wallet.getTransactionHistory()
Collaborative exit or "offboarding" allows you to withdraw your virtual funds to an on-chain address:
import { Ramps } from '@arkade-os/sdk'
const exitTxid = await new Ramps(wallet).offboard(onchainAddress);
Unilateral exit allows you to withdraw your funds from the Ark protocol back to the Bitcoin blockchain without requiring cooperation from the Ark server. This process involves two main steps:
- Unrolling: Broadcasting the transaction chain from off-chain back to on-chain
- Completing the exit: Spending the unrolled VTXOs after the timelock expires
import { Unroll, OnchainWallet, SingleKey } from '@arkade-os/sdk'
// Create an identity for the onchain wallet
const onchainIdentity = SingleKey.fromHex('your_onchain_private_key_hex');
// Create an onchain wallet to pay for P2A outputs in VTXO branches
// OnchainWallet implements the AnchorBumper interface
const onchainWallet = await OnchainWallet.create(onchainIdentity, 'regtest');
// Unroll a specific VTXO
const vtxo = { txid: 'your_vtxo_txid', vout: 0 };
const session = await Unroll.Session.create(
vtxo,
onchainWallet,
onchainWallet.provider,
wallet.indexerProvider
);
// Iterate through the unrolling steps
for await (const step of session) {
switch (step.type) {
case Unroll.StepType.WAIT:
console.log(`Waiting for transaction ${step.txid} to be confirmed`);
break;
case Unroll.StepType.UNROLL:
console.log(`Broadcasting transaction ${step.tx.id}`);
break;
case Unroll.StepType.DONE:
console.log(`Unrolling complete for VTXO ${step.vtxoTxid}`);
break;
}
}
The unrolling process works by:
- Traversing the transaction chain from the root (most recent) to the leaf (oldest)
- Broadcasting each transaction that isn't already on-chain
- Waiting for confirmations between steps
- Using P2A (Pay-to-Anchor) transactions to pay for fees
Once VTXOs are fully unrolled and the unilateral exit timelock has expired, you can complete the exit:
// Complete the exit for specific VTXOs
await Unroll.completeUnroll(
wallet,
[vtxo.txid], // Array of VTXO transaction IDs to complete
onchainWallet.address // Address to receive the exit amount
);
Important Notes:
- Each VTXO may require multiple unroll steps depending on the transaction chain length
- Each unroll step must be confirmed before proceeding to the next
- The
completeUnroll
method can only be called after VTXOs are fully unrolled and the timelock has expired - You need sufficient on-chain funds in the
OnchainWallet
to pay for P2A transaction fees
Ultra-simplified setup! We handle all the complex service worker registration and identity management for you:
// SIMPLE SETUP with identity! 🎉
import { ServiceWorkerWallet, SingleKey } from '@arkade-os/sdk';
// Create your identity
const identity = SingleKey.fromHex('your_private_key_hex');
// Or generate a new one:
// const identity = SingleKey.fromRandomBytes();
const wallet = await ServiceWorkerWallet.setup({
serviceWorkerPath: '/service-worker.js',
arkServerUrl: 'https://mutinynet.arkade.sh',
identity
});
// That's it! Ready to use immediately:
const address = await wallet.getAddress();
const balance = await wallet.getBalance();
You'll also need to create a service worker file:
// service-worker.js
import { Worker } from '@arkade-os/sdk'
// Worker handles communication between the main thread and service worker
new Worker().start()
Choose the appropriate storage adapter for your environment:
import {
SingleKey,
Wallet,
InMemoryStorageAdapter, // Works everywhere, data lost on restart
} from '@arkade-os/sdk'
// Import additional storage adapters as needed:
import { LocalStorageAdapter } from '@arkade-os/sdk/adapters/localStorage' // Browser/PWA persistent storage
import { IndexedDBStorageAdapter } from '@arkade-os/sdk/adapters/indexedDB' // Browser/PWA/Service Worker advanced storage
import { AsyncStorageAdapter } from '@arkade-os/sdk/adapters/asyncStorage' // React Native persistent storage
import { FileSystemStorageAdapter } from '@arkade-os/sdk/adapters/fileSystem' // Node.js file-based storage
// Node.js
const storage = new FileSystemStorageAdapter('./wallet-data')
// Browser/PWA
const storage = new LocalStorageAdapter()
// or for advanced features:
const storage = new IndexedDBStorageAdapter('my-app', 1)
// React Native
const storage = new AsyncStorageAdapter()
// Service Worker
const storage = new IndexedDBStorageAdapter('service-worker-wallet', 1)
// Load identity from storage (simple pattern everywhere)
const privateKeyHex = await storage.getItem('private-key')
const identity = SingleKey.fromHex(privateKeyHex)
// Create wallet (same API everywhere)
const wallet = await Wallet.create({
identity,
arkServerUrl: 'https://mutinynet.arkade.sh',
storage // optional
})
For React Native and Expo applications where standard EventSource and fetch streaming may not work properly, use the Expo-compatible providers:
import { Wallet, SingleKey } from '@arkade-os/sdk'
import { ExpoArkProvider, ExpoIndexerProvider } from '@arkade-os/sdk/adapters/expo'
const identity = SingleKey.fromHex('your_private_key_hex')
const wallet = await Wallet.create({
identity: identity,
esploraUrl: 'https://mutinynet.com/api',
arkProvider: new ExpoArkProvider('https://mutinynet.arkade.sh'), // For settlement events and transactions streaming
indexerProvider: new ExpoIndexerProvider('https://mutinynet.arkade.sh'), // For address subscriptions and VTXO updates
})
// use expo/fetch for streaming support (SSE)
// All other wallet functionality remains the same
const balance = await wallet.getBalance()
const address = await wallet.getAddress()
Both ExpoArkProvider and ExpoIndexerProvider are available as adapters following the SDK's modular architecture pattern. This keeps the main SDK bundle clean while providing opt-in functionality for specific environments:
- ExpoArkProvider: Handles settlement events and transaction streaming using expo/fetch for Server-Sent Events
- ExpoIndexerProvider: Handles address subscriptions and VTXO updates using expo/fetch for JSON streaming
Install expo-crypto
and polyfill crypto.getRandomValues()
at the top of your app entry point:
npx expo install expo-crypto
// App.tsx or index.js - MUST be first import
import * as Crypto from 'expo-crypto';
if (!global.crypto) global.crypto = {} as any;
global.crypto.getRandomValues = Crypto.getRandomValues;
// Now import the SDK
import { Wallet, SingleKey } from '@arkade-os/sdk';
import { ExpoArkProvider, ExpoIndexerProvider } from '@arkade-os/sdk/adapters/expo';
This is required for MuSig2 settlements and cryptographic operations.
Access low-level data management through repositories:
// VTXO management (automatically cached for performance)
const addr = await wallet.getAddress()
const vtxos = await wallet.walletRepository.getVtxos(addr)
await wallet.walletRepository.saveVtxos(addr, vtxos)
// Contract data for SDK integrations
await wallet.contractRepository.setContractData('my-contract', 'status', 'active')
const status = await wallet.contractRepository.getContractData('my-contract', 'status')
// Collection management for related data
await wallet.contractRepository.saveToContractCollection(
'swaps',
{ id: 'swap-1', amount: 50000, type: 'reverse' },
'id' // key field
)
const swaps = await wallet.contractRepository.getContractCollection('swaps')
For complete API documentation, visit our TypeScript documentation.
-
Install dependencies:
pnpm install pnpm format pnpm lint
-
Install nigiri for integration tests:
curl https://getnigiri.vulpem.com | bash
# Run all tests
pnpm test
# Run unit tests only
pnpm test:unit
# Run integration tests with ark provided by nigiri
nigiri start --ark
pnpm test:setup # Run setup script for integration tests
pnpm test:integration
nigiri stop --delete
# Run integration tests with ark provided by docker (requires nigiri)
nigiri start
pnpm test:up-docker
pnpm test:setup-docker # Run setup script for integration tests
pnpm test:integration-docker
pnpm test:down-docker
nigiri stop --delete
# Watch mode for development
pnpm test:watch
# Run tests with coverage
pnpm test:coverage
# Build the TypeScript documentation
pnpm docs:build
# Open the docs in the browser
pnpm docs:open
# Release new version (will prompt for version patch, minor, major)
pnpm release
# You can test release process without making changes
pnpm release:dry-run
# Cleanup: checkout version commit and remove release branch
pnpm release:cleanup
MIT