Skip to main content

Overview

Unilateral exits allow you to move your Ark funds back to the Bitcoin blockchain without requiring server cooperation. This is a critical safety feature that ensures you always maintain full control of your funds.
Exits are expensive (onchain fees) and slow (requires confirmations). Only exit when:
  • The server is unresponsive or malicious
  • Your VTXOs are about to expire
  • You need funds onchain and can’t offboard normally
For normal onchain sends, use offboard instead.

Exit System Architecture

The Exit subsystem manages the entire unilateral exit lifecycle:
// Access the exit system
let mut exit = wallet.exit.write().await;

// The exit system tracks:
// - Which VTXOs are being exited
// - The current state of each exit
// - Related onchain transactions
// - Historical state transitions

Starting an Exit

Exit Entire Wallet

Exit all VTXOs at once:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Sync wallet first
    wallet.maintenance().await?;

    // Get exit lock
    let mut exit = wallet.exit.write().await;

    // Start exit for all VTXOs
    exit.start_exit_for_entire_wallet().await?;

    println!("Exit initiated for entire wallet");

    Ok(())
}

Exit Specific VTXOs

// Get VTXOs to exit
let vtxos = wallet.spendable_vtxos().await?;
let to_exit = vtxos.into_iter()
    .filter(|v| v.amount() > Amount::from_sat(1000))
    .collect::<Vec<_>>();

// Start exit
let mut exit = wallet.exit.write().await;
exit.start_exit_for_vtxos(&to_exit).await?;

println!("Started exit for {} VTXOs", to_exit.len());
VTXOs smaller than the dust limit (330 sats) cannot be exited and will be rejected.

Progressing Exits

After starting an exit, you must periodically progress it:
use bark::onchain::OnchainWallet;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut onchain_wallet = OnchainWallet::load_or_create(
        network, seed, db.clone()
    ).await?;

    // Get exit lock
    let mut exit = wallet.exit.write().await;

    // Progress all pending exits
    let statuses = exit.progress_exits(
        &wallet,
        &mut onchain_wallet,
        None // use automatic fee rate
    ).await?;

    if let Some(statuses) = statuses {
        for status in statuses {
            println!("VTXO {}: {:?}", status.vtxo_id, status.state);
            if let Some(error) = status.error {
                println!("Error: {}", error);
            }
        }
    }

    Ok(())
}

Custom Fee Rate

Override the automatic fee rate:
use bitcoin::FeeRate;

// Use a custom fee rate (10 sat/vB)
let fee_rate = FeeRate::from_sat_per_vb(10).unwrap();

exit.progress_exits(
    &wallet,
    &mut onchain_wallet,
    Some(fee_rate)
).await?;

Exit States

Exits progress through multiple states:
use bark::exit::{ExitState, ExitVtxo};

let exit_vtxos = wallet.exit.read().await.get_exit_vtxos();

for exit_vtxo in exit_vtxos {
    println!("VTXO {}", exit_vtxo.id());
    
    match exit_vtxo.state() {
        ExitState::Start(_) => {
            println!("  Status: Exit started, not yet broadcast");
        }
        ExitState::Processing(_) => {
            println!("  Status: Transaction broadcast, waiting for confirmations");
        }
        ExitState::AwaitingDelta(_) => {
            println!("  Status: Confirmed, waiting for VTXO exit delta");
        }
        ExitState::Claimable(_) => {
            println!("  Status: Ready to claim!");
        }
        ExitState::ClaimInProgress(_) => {
            println!("  Status: Claim transaction broadcast");
        }
        ExitState::Claimed(_) => {
            println!("  Status: Successfully claimed");
        }
    }
}

Syncing Exits

Sync exit status without progressing them:
// Sync exit transactions (updates status)
exit.sync_exits(&onchain_wallet).await?;

// Or sync without requiring mutable wallet reference
exit.sync_no_progress(&onchain_wallet).await?;
During maintenance, exits are automatically synced and progressed:
// Syncs and progresses exits
wallet.maintenance_with_onchain(&mut onchain_wallet).await?;

Querying Exit Status

Get Specific Exit

use ark::VtxoId;

let vtxo_id = VtxoId::from_slice(&[...])?;

// Get detailed exit status
let status = wallet.exit.read().await.get_exit_status(
    vtxo_id,
    true,  // include history
    true   // include transactions
).await?;

if let Some(status) = status {
    println!("VTXO {}", status.vtxo_id);
    println!("State: {:?}", status.state);
    
    if let Some(history) = status.history {
        println!("State history: {} transitions", history.len());
    }
    
    for tx_package in status.transactions {
        println!("Exit TX: {}", tx_package.exit.txid);
        if let Some(child) = tx_package.child {
            println!("  Child TX: {}", child.txid);
        }
    }
}

List All Exits

let exit = wallet.exit.read().await;

// Get all exit VTXOs
let all_exits = exit.get_exit_vtxos();
println!("Total exits: {}", all_exits.len());

// Get specific VTXO exit
if let Some(exit_vtxo) = exit.get_exit_vtxo(vtxo_id) {
    println!("Found exit for VTXO {}", exit_vtxo.id());
    println!("Amount: {}", exit_vtxo.amount());
    println!("State: {:?}", exit_vtxo.state());
}

Check Exit Status

let exit = wallet.exit.read().await;

// Check if there are pending exits
if exit.has_pending_exits() {
    let pending_amount = exit.pending_total();
    println!("Pending exits: {}", pending_amount);
}

// Get claimable height
if let Some(height) = exit.all_claimable_at_height().await {
    println!("All exits claimable at height {}", height);
}

Claiming Exited Funds

Once an exit is claimable, create a transaction to claim the funds:

List Claimable Exits

let exit = wallet.exit.read().await;
let claimable = exit.list_claimable();

println!("Claimable exits: {}", claimable.len());
for exit_vtxo in &claimable {
    println!("VTXO {}: {}", exit_vtxo.id(), exit_vtxo.amount());
}

Drain to Address

Create a transaction that claims all exits to a single address:
use bitcoin::Address;

let exit = wallet.exit.read().await;
let claimable = exit.list_claimable();

if claimable.is_empty() {
    println!("No claimable exits yet");
} else {
    // Destination for claimed funds
    let destination = Address::from_str("bc1p...")?.assume_checked();
    
    // Create drain PSBT
    let psbt = exit.drain_exits(
        &claimable,
        &wallet,
        destination,
        None // use automatic fee rate
    ).await?;
    
    // Extract and broadcast
    let tx = psbt.extract_tx()?;
    println!("Claim transaction: {}", tx.compute_txid());
    
    // Broadcast the transaction
    wallet.chain.broadcast_tx(&tx).await?;
    println!("Claim broadcast!");
}

Custom Fee Rate for Claims

let fee_rate = FeeRate::from_sat_per_vb(20).unwrap();

let psbt = exit.drain_exits(
    &claimable,
    &wallet,
    destination,
    Some(fee_rate)
).await?;

Complete Exit Workflow

use bark::{Config, Wallet, SqliteClient};
use bark::onchain::OnchainWallet;
use bark::exit::ExitState;
use bitcoin::{Address, FeeRate};
use std::sync::Arc;
use std::str::FromStr;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Setup
    let network = bitcoin::Network::Signet;
    let mnemonic = bip39::Mnemonic::from_str(&mnemonic_str)?;
    let config = Config::network_default(network);
    let db = Arc::new(SqliteClient::open("db.sqlite").await?);

    let mut onchain_wallet = OnchainWallet::load_or_create(
        network, mnemonic.to_seed(""), db.clone()
    ).await?;

    let wallet = Wallet::open_with_onchain(
        &mnemonic, db, &onchain_wallet, config
    ).await?;

    // Sync first
    wallet.maintenance().await?;

    // Start exit for entire wallet
    {
        let mut exit = wallet.exit.write().await;
        exit.start_exit_for_entire_wallet().await?;
        println!("Exit started for all VTXOs");
    }

    // Progress exits until claimable
    loop {
        {
            let mut exit = wallet.exit.write().await;
            
            // Sync and progress
            exit.sync(&wallet, &mut onchain_wallet).await?;
            exit.progress_exits(&wallet, &mut onchain_wallet, None).await?;
            
            // Check status
            let claimable = exit.list_claimable();
            let all_exits = exit.get_exit_vtxos();
            
            println!("Total exits: {}", all_exits.len());
            println!("Claimable: {}", claimable.len());
            
            if claimable.len() == all_exits.len() {
                println!("All exits are claimable!");
                break;
            }
            
            if exit.has_pending_exits() {
                println!("Pending: {}", exit.pending_total());
            }
        }
        
        // Wait before next check
        tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
    }

    // Claim all exits
    {
        let exit = wallet.exit.read().await;
        let claimable = exit.list_claimable();
        
        let destination = Address::from_str("bc1p...")?.assume_checked();
        let psbt = exit.drain_exits(&claimable, &wallet, destination, None).await?;
        
        let tx = psbt.extract_tx()?;
        println!("Broadcasting claim: {}", tx.compute_txid());
        
        wallet.chain.broadcast_tx(&tx).await?;
        println!("Exit complete!");
    }

    Ok(())
}

Advanced: Sign Exit Inputs

Manually sign PSBT inputs that are exit claims:
use bitcoin::Psbt;

// Assuming you have a PSBT with exit claim inputs
let mut psbt: Psbt = /* ... */;

let exit = wallet.exit.read().await;
exit.sign_exit_claim_inputs(&mut psbt, &wallet).await?;

println!("Exit inputs signed");

Error Handling

use bark::exit::ExitError;

match exit.start_exit_for_vtxos(&vtxos).await {
    Ok(()) => println!("Exit started"),
    Err(e) if e.to_string().contains("dust") => {
        println!("VTXO too small to exit (below dust limit)");
    }
    Err(e) => return Err(e),
}

match exit.progress_exits(&wallet, &mut onchain_wallet, None).await {
    Ok(Some(statuses)) => {
        for status in statuses {
            if let Some(error) = status.error {
                match error {
                    ExitError::InsufficientConfirmedFunds { .. } => {
                        println!("Need more confirmed onchain funds for CPFP");
                    }
                    _ => println!("Exit error: {}", error),
                }
            }
        }
    }
    Ok(None) => println!("No exits to progress"),
    Err(e) => return Err(e),
}

Best Practices

1

Sync before exiting

Always sync your wallet before starting an exit to ensure you’re working with current data:
wallet.maintenance().await?;
2

Monitor exit progress

Regularly progress exits until they’re claimable. Set up a periodic task:
loop {
    let mut exit = wallet.exit.write().await;
    exit.progress_exits(&wallet, &mut onchain_wallet, None).await?;
    tokio::time::sleep(tokio::time::Duration::from_secs(600)).await; // every 10 min
}
3

Have onchain funds available

Exits require onchain funds for CPFP (Child Pays For Parent) fee bumping. Ensure your onchain wallet has confirmed funds.
4

Don't exit dust

Small VTXOs cost more in fees than they’re worth. Avoid exiting VTXOs below ~10,000 sats unless absolutely necessary.
5

Use appropriate fee rates

During high fee periods, consider using custom fee rates to avoid overpaying:
let fee_rate = FeeRate::from_sat_per_vb(5).unwrap();
exit.progress_exits(&wallet, &mut onchain_wallet, Some(fee_rate)).await?;
Exits create movements that cannot currently be canceled. Once you start an exit, you must complete it or your VTXOs will eventually expire and be lost.

Exit Timeline

Typical exit timeline (may vary):
  1. Start → Immediate
  2. Broadcast → Few seconds
  3. First confirmation → ~10 minutes
  4. Exit delta wait → Server-specific (typically 1-6 blocks)
  5. Claimable → Ready to drain
  6. Claim broadcast → Few seconds
  7. Claim confirmed → ~10 minutes
Total time: Usually 30-60 minutes, depending on network conditions and server settings.

Next Steps

Sending Payments

Return to normal operations after exit

Boarding Funds

Re-board funds to Ark after claiming

Build docs developers (and LLMs) love