Overview
Bark provides two ways to move funds from Ark to onchain Bitcoin:
- Offboarding: Cooperative exit with the server (fast and cheap)
- Unilateral Exit: Non-cooperative exit without server (trustless but slower)
Offboarding (Cooperative)
Offboarding works with the Ark server to efficiently move funds onchain.
offboard_all
Offboard all spendable VTXOs to a Bitcoin address.
pub async fn offboard_all(&self, address: bitcoin::Address) -> anyhow::Result<Txid>
Bitcoin address to receive funds
Transaction ID of the offboard transaction
Example:
use bitcoin::Address;
let addr = Address::from_str("bc1q...")?.assume_checked();
let txid = wallet.offboard_all(addr).await?;
println!("Offboard tx: {}", txid);
offboard_vtxos
Offboard specific VTXOs to a Bitcoin address.
pub async fn offboard_vtxos<V: VtxoRef>(
&self,
vtxos: impl IntoIterator<Item = V>,
address: bitcoin::Address,
) -> anyhow::Result<Txid>
vtxos
impl IntoIterator<Item = V>
required
VTXOs to offboard
Destination Bitcoin address
Example:
// Select specific VTXOs to offboard
let vtxos = wallet.spendable_vtxos().await?;
let to_offboard = vtxos.into_iter().take(3).collect::<Vec<_>>();
let txid = wallet.offboard_vtxos(to_offboard, addr).await?;
Offboard fees are deducted from the total VTXO amount. The server calculates fees based on the output script and current fee rates.
Unilateral Exit (Non-Cooperative)
Unilateral exits allow you to claim funds onchain without server cooperation. The Exit subsystem is accessed through wallet.exit.
Access Exit Subsystem
// Get read lock
let exit = wallet.exit.read().await;
// Get write lock for mutations
let mut exit = wallet.exit.write().await;
Starting Exits
start_exit_for_entire_wallet
Mark all VTXOs for unilateral exit.
pub async fn start_exit_for_entire_wallet(&mut self) -> anyhow::Result<()>
Example:
let mut exit = wallet.exit.write().await;
exit.start_exit_for_entire_wallet().await?;
start_exit_for_vtxos
Mark specific VTXOs for unilateral exit.
pub async fn start_exit_for_vtxos(
&mut self,
vtxos: &[impl AsRef<Vtxo>]
) -> anyhow::Result<()>
vtxos
&[impl AsRef<Vtxo>]
required
VTXOs to exit
Example:
let vtxos = wallet.spendable_vtxos().await?;
let to_exit = vtxos.iter().take(2).collect::<Vec<_>>();
let mut exit = wallet.exit.write().await;
exit.start_exit_for_vtxos(&to_exit).await?;
Progressing Exits
sync_exits
Sync transaction status of unilateral exits.
pub async fn sync_exits(
&self,
onchain: &mut dyn ExitUnilaterally,
) -> anyhow::Result<()>
onchain
&mut dyn ExitUnilaterally
required
Onchain wallet for exit operations
progress_exits
Advance exit state machine (broadcast, fee-bump, etc.).
pub async fn progress_exits(
&mut self,
wallet: &Wallet,
onchain: &mut impl ExitUnilaterally,
fee_rate: Option<FeeRate>,
) -> anyhow::Result<()>
Reference to the Bark wallet
onchain
&mut impl ExitUnilaterally
required
Onchain wallet for signing transactions
Override fee rate (uses network estimate if None)
Example:
use bark::onchain::OnchainWallet;
let mut onchain = OnchainWallet::load_or_create(
network, seed, db.clone()
).await?;
// Start exit
let mut exit_lock = wallet.exit.write().await;
exit_lock.start_exit_for_entire_wallet().await?;
// Progress periodically until complete
loop {
exit_lock.sync_no_progress(&onchain).await?;
exit_lock.progress_exits(&wallet, &mut onchain, None).await?;
if !exit_lock.has_pending_exits() {
break;
}
tokio::time::sleep(Duration::from_secs(60)).await;
}
Exit transactions may need to wait for the VTXO exit delta period before they can be broadcast. Continue calling progress_exits until completion.
Exit Status
get_exit_status
Get detailed status of a VTXO exit.
pub async fn get_exit_status(
&self,
vtxo_id: VtxoId,
include_history: bool,
include_transactions: bool,
) -> Result<Option<ExitTransactionStatus>, ExitError>
Include state machine history
Include transaction details
pub struct ExitTransactionStatus {
pub vtxo_id: VtxoId,
pub state: ExitState,
pub history: Option<Vec<ExitState>>,
pub transactions: Vec<ExitTransactionPackage>,
}
Example:
let exit = wallet.exit.read().await;
if let Some(status) = exit.get_exit_status(vtxo_id, true, true).await? {
println!("State: {:?}", status.state);
println!("Transactions: {}", status.transactions.len());
}
get_exit_vtxos
Get all VTXOs in the exit system.
pub fn get_exit_vtxos(&self) -> &Vec<ExitVtxo>
pub struct ExitVtxo {
// VTXO being exited
// Current exit state
// State history
// Associated transactions
}
has_pending_exits
Check if any exits are in progress.
pub fn has_pending_exits(&self) -> bool
Example:
let exit = wallet.exit.read().await;
if exit.has_pending_exits() {
println!("Exits still in progress");
}
pending_total
Get total amount in pending exits.
pub fn pending_total(&self) -> Amount
Claiming Exits
list_claimable
Get all exits that are ready to claim.
pub fn list_claimable(&self) -> Vec<VtxoId>
VTXO IDs that are claimable (fully confirmed onchain)
drain_exits
Create a transaction to claim exited funds.
pub async fn drain_exits(
&mut self,
exit_vtxos: &[VtxoId],
wallet: &Wallet,
destination: Address,
fee_rate: Option<FeeRate>,
) -> anyhow::Result<Psbt>
VTXOs to claim (must be claimable)
Where to send claimed funds
Signed PSBT ready to broadcast
Example:
let mut exit = wallet.exit.write().await;
// Get claimable exits
let claimable = exit.list_claimable();
if !claimable.is_empty() {
let drain_addr = Address::from_str("bc1q...")?.assume_checked();
let psbt = exit.drain_exits(
&claimable,
&wallet,
drain_addr,
None
).await?;
// Broadcast the PSBT
let tx = psbt.extract_tx()?;
wallet.chain.broadcast_tx(&tx).await?;
}
Exit States
The exit state machine progresses through these states:
pub enum ExitState {
Start(ExitStartState),
Processing(ExitProcessingState),
AwaitingDelta(ExitAwaitingDeltaState),
Claimable(ExitClaimableState),
ClaimInProgress(ExitClaimInProgressState),
Claimed(ExitClaimedState),
}
State Descriptions:
- Start: Exit initiated, not yet processed
- Processing: Transaction created, awaiting broadcast
- AwaitingDelta: Waiting for exit delta period to elapse
- Claimable: Confirmed onchain, ready to claim
- ClaimInProgress: Claim transaction broadcast
- Claimed: Funds successfully claimed
Complete Exit Example
use bark::{Wallet, onchain::OnchainWallet};
use bitcoin::Address;
use std::time::Duration;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Load wallets
let wallet = Wallet::open(&mnemonic, db.clone(), config).await?;
let mut onchain = OnchainWallet::load_or_create(
network, seed, db
).await?;
// Start exit
let mut exit = wallet.exit.write().await;
exit.start_exit_for_entire_wallet().await?;
drop(exit); // Release lock
// Progress until complete
loop {
let mut exit = wallet.exit.write().await;
exit.sync_no_progress(&onchain).await?;
exit.progress_exits(&wallet, &mut onchain, None).await?;
if !exit.has_pending_exits() {
// All exits claimable
let claimable = exit.list_claimable();
let drain_addr = Address::from_str("bc1q...")?.assume_checked();
let psbt = exit.drain_exits(
&claimable,
&wallet,
drain_addr,
None
).await?;
let tx = psbt.extract_tx()?;
wallet.chain.broadcast_tx(&tx).await?;
println!("Exit complete: {}", tx.compute_txid());
break;
}
drop(exit);
tokio::time::sleep(Duration::from_secs(60)).await;
}
Ok(())
}
Comparison: Offboard vs Exit
| Feature | Offboard | Unilateral Exit |
|---|
| Speed | Fast (minutes) | Slow (hours/days) |
| Cost | Low fees | Higher fees |
| Server | Required | Not required |
| Trust | Server must cooperate | Fully trustless |
| Use Case | Normal operation | Emergency/dispute |
Use offboarding when possible. Only use unilateral exits when the server is unavailable or uncooperative.