Skip to content

Chain swap via Boltz #187

@luckysori

Description

@luckysori

Overview

Add chain swap support (ARK ↔ BTC) to the rust-sdk ark-client, mirroring the feature already implemented in boltz-swap (TypeScript). Chain swaps allow direct on-chain BTC ↔ Ark VTXO swaps via Boltz without routing through Lightning.

This is the Rust equivalent of arkToBtc() / btcToArk() in boltz-swap/src/arkade-swaps.ts.


Background

The rust-sdk currently supports two Boltz swap types in ark-client/src/boltz.rs:

  • Submarine swap (ARK → Lightning): pay_ln_invoice(), refund_expired_vhtlc(), refund_vhtlc()
  • Reverse submarine swap (Lightning → ARK): get_ln_invoice(), claim_vhtlc(), wait_for_vhtlc()

Chain swaps are a third type:

  • ARK → BTC: User locks a VHTLC on Ark; Boltz claims it and locks BTC; user claims BTC via MuSig2 HTLC
  • BTC → ARK: User sends on-chain BTC to a Boltz HTLC address; Boltz locks a VHTLC on Ark; user claims the VHTLC

Both flows use the Boltz v2 chain swap API (POST /v2/swap/chain). The TypeScript implementation in boltz-swap is the reference.


Affected Components

File Change Needed
ark-client/src/boltz.rs Add chain swap structs, API types, and methods
ark-client/src/swap_storage/mod.rs Add ChainSwapData storage trait methods
ark-client/src/swap_storage/memory.rs Implement in-memory storage for ChainSwapData
ark-client/src/swap_storage/sqlite.rs Implement SQLite persistence for ChainSwapData
ark-client/Cargo.toml Likely no new deps; may need boltz-client or existing crypto crates

New Types to Add

/// Data for a pending chain swap (ARK ↔ BTC).
pub struct ChainSwapData {
    pub id: String,
    pub status: SwapStatus,
    pub direction: ChainSwapDirection,
    /// Ephemeral keypair used for MuSig2 claim (BTC side)
    pub ephemeral_key: secp256k1::SecretKey,
    /// Preimage (set after claim, for BTC→ARK)
    pub preimage: Option<[u8; 32]>,
    pub preimage_hash: ripemd160::Hash,
    pub lockup_details: ChainLockupDetails,
    pub claim_details: ChainClaimDetails,
    pub fee_sats_per_vbyte: u64,
    pub created_at: u64,
}

pub enum ChainSwapDirection {
    ArkToBtc { btc_address: bitcoin::Address },
    BtcToArk,
}

/// Result of a successful ARK → BTC chain swap initiation.
pub struct ArkToBtcResult {
    pub swap_id: String,
    /// Amount to lock on Ark (send to ark_address)
    pub amount: Amount,
    /// Ark VHTLC address to fund
    pub ark_address: ArkAddress,
    pub pending_swap: ChainSwapData,
}

/// Result of a successful BTC → ARK chain swap initiation.
pub struct BtcToArkResult {
    pub swap_id: String,
    /// Amount of BTC to send
    pub amount: Amount,
    /// On-chain BTC address to fund
    pub btc_address: bitcoin::Address,
    pub pending_swap: ChainSwapData,
}

New Methods to Add to Client<B, W, S, K>

ARK → BTC

/// Create a chain swap from ARK to BTC.
/// Equivalent to `arkade-swaps.ts: arkToBtc()`.
pub async fn ark_to_btc(
    &self,
    btc_address: bitcoin::Address,
    amount: SwapAmount,
    fee_rate: u64,
) -> Result<ArkToBtcResult, Error>;

/// After funding the VHTLC, wait for Boltz to lock BTC and claim it.
/// Equivalent to `arkade-swaps.ts: waitAndClaimBtc()`.
pub async fn wait_and_claim_btc(
    &self,
    swap_id: &str,
) -> Result<Txid, Error>;

/// Claim BTC using MuSig2 once Boltz has locked funds.
/// Equivalent to `arkade-swaps.ts: claimBtc()`.
pub async fn claim_btc(&self, swap: &ChainSwapData) -> Result<Txid, Error>;

/// If ARK→BTC swap fails, refund the VHTLC back to self.
/// Equivalent to `arkade-swaps.ts: refundArk()`.
pub async fn refund_ark_chain_swap(&self, swap_id: &str) -> Result<Txid, Error>;

BTC → ARK

/// Create a chain swap from BTC to ARK.
/// Equivalent to `arkade-swaps.ts: btcToArk()`.
pub async fn btc_to_ark(
    &self,
    amount: SwapAmount,
    fee_rate: u64,
) -> Result<BtcToArkResult, Error>;

/// After funding the BTC HTLC, wait for Boltz to lock a VHTLC and claim it.
/// Equivalent to `arkade-swaps.ts: waitAndClaimArk()`.
pub async fn wait_and_claim_ark(
    &self,
    swap_id: &str,
) -> Result<ClaimVhtlcResult, Error>;

/// Claim the VHTLC on Ark once Boltz has locked it.
/// Equivalent to `arkade-swaps.ts: claimArk()`.
pub async fn claim_ark_chain_swap(&self, swap: &ChainSwapData) -> Result<ClaimVhtlcResult, Error>;

Boltz API Types to Add

// POST /v2/swap/chain
struct CreateChainSwapRequest {
    from: Asset,
    to: Asset,
    preimage_hash: sha256::Hash,
    claim_public_key: Option<PublicKey>,   // BTC claim key (ARK→BTC: Boltz's; BTC→ARK: ours, MuSig2)
    refund_public_key: Option<PublicKey>,  // refund key
    to_address: Option<String>,            // destination BTC address (ARK→BTC)
    fee_rate: Option<u64>,
    server_lock_amount: Option<u64>,
    user_lock_amount: Option<u64>,
}

struct CreateChainSwapResponse {
    id: String,
    preimage_hash: sha256::Hash,
    claim_details: ChainSideDetails,    // the side we claim
    lockup_details: ChainSideDetails,   // the side we fund
}

struct ChainSideDetails {
    swap_tree: Option<SwapTree>,
    lockup_address: String,
    server_public_key: Option<String>,
    timeout_block_height: u32,
    amount: u64,
}

// POST /v2/swap/chain/{id}/claim  (ARK→BTC: post MuSig2 partial sig to get server's)
struct PostChainClaimDetailsRequest {
    preimage: Option<String>,
    to_sign: ToSignDetails,
}

Implementation Notes

ARK → BTC flow

  1. Call POST /v2/swap/chain (from: Ark, to: Btc) → get lockupDetails.lockupAddress (VHTLC on Ark) and claimDetails (BTC HTLC)
  2. Fund the VHTLC: reuse existing pay_vhtlc/offchain-tx logic from submarine swap
  3. Monitor swap status: wait for transaction.server.mempool / transaction.server.confirmed
  4. Construct BTC claim tx, sign with MuSig2 (ephemeral key + Boltz server key), post to /v2/swap/chain/{id}/claim
  5. If expired: refund via refund_ark_chain_swap() — uses same VHTLC refund path as refund_vhtlc()

BTC → ARK flow

  1. Call POST /v2/swap/chain (from: Btc, to: Ark) → get lockupDetails.lockupAddress (BTC on-chain HTLC) and claimDetails (VHTLC on Ark)
  2. User sends BTC to lockupDetails.lockupAddress externally
  3. Monitor swap status: wait for transaction.server.mempool / transaction.server.confirmed
  4. Claim the VHTLC: reuse existing claim_vhtlc() logic from reverse swap

MuSig2 for BTC claim

The BTC claim transaction uses a MuSig2 aggregate key (ephemeral key + Boltz server key). This is the same pattern as in boltz-swap/src/arkade-swaps.ts: claimBtc(). The existing codebase has taproot/MuSig2 primitives in ark-core.

Fee fetching

The existing get_fees() method should be extended to include ChainFeesResponse (already has SubmarineSwapFees and ReverseSwapFees).


Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions