Skip to main content
Arkade integrates with Boltz chain swaps so you can move funds between Bitcoin onchain UTXOs and Arkade VTXOs.

Overview

The BoltzSwapProvider extends Arkade contracts with two chain swap flows:
  1. Bitcoin to Arkade swaps - Receive BTC onchain and claim into your Arkade wallet
  2. Arkade to Bitcoin swaps - Send from your Arkade wallet to a Bitcoin address
This guide mirrors the Lightning swap workflow and uses the same @arkade-os/boltz-swap package.

Installation

npm install @arkade-os/sdk @arkade-os/boltz-swap

Basic usage

Initializing the Chain Swap Provider

import { Wallet } from '@arkade-os/sdk'
import { ArkadeSwaps, BoltzSwapProvider } from '@arkade-os/boltz-swap'

// Initialize your Arkade wallet
const wallet = await Wallet.create({
  identity,
  arkServerUrl: 'https://arkade.computer',
})

// Initialize the chain swap provider
const swapProvider = new BoltzSwapProvider({
  apiUrl: 'https://api.ark.boltz.exchange',
  network: 'bitcoin',
})

// Create chainSwaps instance
const chainSwaps = new ArkadeSwaps({
  wallet,
  swapProvider,
})

Checking limits

Before creating a chain swap, validate the supported range:
const limits = await chainSwaps.getLimits('ARK', 'BTC')
// => { min: number, max: number }

if (limits) {
  console.log('Minimum:', limits.min, 'sats')
  console.log('Maximum:', limits.max, 'sats')

  const amount = 50_000
  if (amount < limits.min) {
    console.error(`Amount ${amount} is below minimum ${limits.min}`)
  } else if (amount > limits.max) {
    console.error(`Amount ${amount} is above maximum ${limits.max}`)
  }
}

Checking fees

You can estimate fees for a proposed chain swap amount:
const calculateSwapFee = async (satoshis: number): Promise<number> => {
  if (!satoshis) return 0

  const fees = await chainSwaps.getFees('ARK', 'BTC')
  // => { percentage: number, minerFees: { server: number, user: { claim: number, lockup: number } } }
  
  if (!fees) throw new Error('Unable to fetch fees')

  return Math.ceil(
    (satoshis * fees.percentage) / 100 + fees.minerFees.server + fees.minerFees.user.claim + fees.minerFees.user.lockup,
  )
}

Receiving BTC payments (Bitcoin → Arkade)

When setting amount, choose one mode:
  • senderLockAmount: sender pays exactly this amount; you receive less after fees
  • receiverLockAmount: you receive exactly this amount; sender pays more including fees
const result = await chainSwaps.btcToArk({
  receiverLockAmount: 5_000,
})
// => { btcAddress: string, amountToPay: number, pendingSwap: { id: string; preimage: string; amount: number; /* more fields per PendingChainSwap */  } }

console.log('BTC deposit address:', result.btcAddress)
console.log('Amount to pay:', result.amountToPay)
console.log('Pending swap:', result.pendingSwap.id)

Monitoring incoming BTC payments

const { txid: claimTxid } = await chainSwaps.waitAndClaimArk(result.pendingSwap)
console.log('Claim successful:', claimTxid)

Sending BTC payments (Arkade → Bitcoin)

const result = await chainSwaps.arkToBtc({
  receiverLockAmount: 5_000,
})
// => { arkAddress: string, amountToPay: number, pendingSwap: { id: string; preimage: string; amount: number; /* more fields per PendingChainSwap */  } }

const txid = await wallet.send({
  address: result.arkAddress,
  amount: result.amountToPay,
})

Monitoring outgoing BTC payments

const { txid: payoutTxid } = await chainSwaps.waitAndClaimBtc(result.pendingSwap)
console.log('Payout successful:', payoutTxid)

Checking swap status

const { status } = await chainSwaps.getSwapStatus('swap_id')
// => swap.created, swap.expired, transaction.mempool, etc...

console.log('Swap status:', status)

Storage

Swap records are persisted through the wallet contract repository:
const pendingChainSwaps = await chainSwaps.getPendingChainSwaps()
const chainSwapHistory = await chainSwaps.getSwapHistory()
// => Array<{ id: string; preimage: string; amount: number; /* more fields per PendingChainSwap */  }>

Cleanup

ArkadeSwaps implements a disposable lifecycle:
const chainSwaps = new ArkadeSwaps({ wallet, swapProvider })
// ... use it
await chainSwaps.dispose()

Error handling

Handle known swap failures explicitly and refund when possible:
import {
  SwapError,
  SchemaError,
  NetworkError,
  SwapExpiredError,
  InsufficientFundsError,
  TransactionFailedError,
} from '@arkade-os/boltz-swap'

try {
  const result = await chainSwaps.arkToBtc({
    receiverLockAmount: 5_000,
  })

  await wallet.send({
    address: result.arkAddress,
    amount: result.amountToPay,
  })

  await chainSwaps.waitAndClaimBtc(result.pendingSwap)
} catch (error) {
  if (error instanceof InsufficientFundsError) {
    console.error('Insufficient funds:', error.message)
  } else if (error instanceof NetworkError) {
    console.error('Network issue:', error.message)
  } else if (error instanceof SchemaError) {
    console.error('Invalid provider response')
  } else if (error instanceof SwapExpiredError) {
    console.error('Swap expired. Create a new swap request.')
  } else if (error instanceof TransactionFailedError) {
    console.error('Transaction failed')
  } else if (error instanceof SwapError) {
    console.error('Swap failed:', error.message)
  } else {
    console.error('Unknown error:', error)
  }

  if (error instanceof SwapError && error.isRefundable && error.pendingSwap) {
    const { txid: refundTxid } = await chainSwaps.refundArk(error.pendingSwap)
    console.log('Refund claimed:', refundTxid)
  }
}