This page covers the Rust SDK (ark-rs) Lightning integration. For the TypeScript SDK, see Lightning Swaps.
The Arkade Rust SDK enables Lightning Network interoperability through Boltz atomic swaps using
Virtual Hashed Timelock Contracts (VHTLCs). Users can pay Lightning invoices from their
Arkade balance (submarine swaps) and receive Lightning payments into Arkade (reverse swaps).
Installation
Add to your Cargo.toml:
[dependencies]
ark-client = "0.1"
ark-core = "0.1"
Balance: Recoverable VTXOs
offchain_balance() now returns a three-part breakdown:
let balance = client.offchain_balance().await?;
println!("Pre-confirmed : {} sats", balance.pre_confirmed().to_sat());
println!("Confirmed : {} sats", balance.confirmed().to_sat());
println!("Recoverable : {} sats", balance.recoverable().to_sat()); // new
println!("Total : {} sats", balance.total().to_sat());
| Field | Description |
|---|
pre_confirmed() | VTXOs in an in-progress batch, not yet settled |
confirmed() | Settled VTXOs spendable via forfeit + server |
recoverable() | VTXOs that can only be settled (no forfeit required per VTXO) |
total() | Sum of all three |
Recoverable VTXOs are typically expired boarding UTXOs or VHTLCs that were partially spent. They
appear in your total balance but need a settlement round to become fully confirmed.
Submarine Swaps: Paying a Lightning Invoice
A submarine swap lets you pay a Lightning invoice using your Arkade offchain balance.
use lightning_invoice::Bolt11Invoice;
use std::str::FromStr;
let invoice = Bolt11Invoice::from_str("lnbc500u1p...")?;
// One-shot: create swap + fund VHTLC immediately
let result = client.pay_ln_invoice(invoice).await?;
println!("Swap ID: {}", result.swap_id);
println!("VTXO funded: {}", result.txid);
println!("Amount: {} sats", result.amount.to_sat());
// Wait for Boltz to pay the Lightning invoice
client.wait_for_invoice_paid(&result.swap_id).await?;
println!("Invoice paid!");
Two-phase approach (inspect before committing)
// Phase 1: inspect swap parameters
let swap_data = client.prepare_ln_invoice_payment(invoice).await?;
println!("VHTLC address: {}", swap_data.vhtlc_address);
println!("Refund locktime: {}", swap_data.refund_locktime);
// Phase 2: fund (calls send_vtxo internally)
let result = client.pay_ln_invoice(invoice).await?;
Reverse Swaps: Receiving a Lightning Payment
A reverse swap lets you receive a Lightning payment into your Arkade wallet.
use ark_client::boltz::SwapAmount;
use bitcoin::Amount;
// Generate a BOLT11 invoice for 50,000 sats
let result = client
.get_ln_invoice(SwapAmount::Invoice(Amount::from_sat(50_000)), None)
.await?;
println!("Pay this invoice: {}", result.invoice);
println!("Swap ID: {}", result.swap_id);
// Wait for Boltz to lock funds in a VHTLC on Arkade
client.wait_for_vhtlc(&result.swap_id).await?;
// Claim the VHTLC (reveal preimage)
let claim = client.claim_vhtlc(&result.swap_id).await?;
println!("Claimed: {} sats, txid: {}", claim.claim_amount.to_sat(), claim.claim_txid);
SwapAmount variants
| Variant | Description |
|---|
SwapAmount::Invoice(amount) | Specify the invoice amount; Boltz deducts its fee from the VHTLC |
SwapAmount::Vhtlc(amount) | Specify the VHTLC amount; Boltz deducts its fee from the invoice |
Pending Transaction Recovery
Offchain transactions are submitted and finalized in two phases. If the client crashes or loses
connectivity between submit and finalize, the transaction remains in a pending state — it
exists on-server but isn’t confirmed in your wallet.
Recovering regular pending transactions
// List submitted-but-unfinalized offchain transactions
let pending_txs = client.list_vtxos().await?; // or check via ark-client-sample list-pending-txs
// Continue (finalize) all pending offchain transactions
client.continue_pending_txs().await?;
Recovering pending VHTLC spend transactions
When a VHTLC spend (claim or refund) was submitted but not finalized, use:
// List pending VHTLC spend transactions
let pending = client.list_pending_vhtlc_spend_txs().await?;
for tx in &pending {
let swap_id = tx.spend_type.swap_id();
let ark_txid = &tx.pending_tx.ark_txid;
let spend_type = match &tx.spend_type {
PendingVhtlcSpendType::Claim { preimage, .. } => {
format!("claim (preimage: {})", hex::encode(preimage))
}
PendingVhtlcSpendType::CollaborativeRefund { .. } => "collaborative_refund".to_string(),
PendingVhtlcSpendType::ExpiredRefund { .. } => "expired_refund".to_string(),
};
println!("swap_id={swap_id} ark_txid={ark_txid} type={spend_type}");
}
// Finalize a single pending VHTLC spend by swap ID
if let Some(pending_tx) = pending.iter().find(|tx| tx.spend_type.swap_id() == "my-swap-id") {
let txid = client.continue_pending_vhtlc_spend_tx(pending_tx).await?;
println!("Finalized: {txid}");
}
// Or finalize all pending VHTLC spends at once
client.continue_pending_vhtlc_spend_txs().await?;
PendingVhtlcSpendType variants
| Variant | Description | When it occurs |
|---|
Claim { swap_id, preimage } | Receiver claiming a reverse swap VHTLC | Receiving Lightning → Arkade interrupted after submit |
CollaborativeRefund { swap_id } | Sender reclaiming a submarine swap with Boltz cooperation | Paying Lightning failed, Boltz cooperates on refund |
ExpiredRefund { swap_id } | Sender reclaiming via CLTV timeout (no Boltz needed) | Paying Lightning failed, timelock expired |
The Claim variant includes the preimage field. The preimage is persisted in SwapStorage
so it survives restarts. Never log or expose the preimage unnecessarily — it’s the secret
that proves Lightning payment receipt.
Refund Mechanisms
When a submarine swap fails or expires, you can recover funds via refund:
// Check if timelock has expired, then refund unilaterally (no Boltz needed)
let txid = client.refund_expired_vhtlc(&swap_id).await?;
println!("Refunded: {txid}");
// Alternative: join the next Ark settlement round (treats VHTLC as recoverable input)
let txid = client.refund_expired_vhtlc_via_settlement(&mut rng, &swap_id).await?;
println!("Refunded via settlement: {txid}");
// Collaborative refund with Boltz (note: not currently supported by Boltz)
let txid = client.refund_vhtlc(&swap_id).await?;
Fee Behavior
Fees for onchain transactions (e.g., unilateral exits) are deducted from change, not from
the send amount. This means:
send_amount in logs/results reflects the exact amount sent to the recipient
- Change output is reduced by the fee
- If change is insufficient to cover the fee, the transaction returns an insufficient-balance error
For LLM agents
Key types in ark-client:
OffChainBalance — fields: pre_confirmed(), confirmed(), recoverable(), total()
PendingVhtlcSpendType — enum variants: Claim { swap_id, preimage }, CollaborativeRefund { swap_id }, ExpiredRefund { swap_id }
SubmarineSwapResult — fields: swap_id, txid, amount
ReverseSwapResult — fields: swap_id, amount, invoice
ClaimVhtlcResult — fields: swap_id, claim_txid, claim_amount, preimage
Key methods on Client:
pay_ln_invoice(invoice) → SubmarineSwapResult
prepare_ln_invoice_payment(invoice) → SubmarineSwapData
wait_for_invoice_paid(swap_id) → ()
get_ln_invoice(amount, expiry_secs) → ReverseSwapResult
claim_vhtlc(swap_id) → ClaimVhtlcResult
list_pending_vhtlc_spend_txs() → Vec<PendingVhtlcSpend>
continue_pending_vhtlc_spend_tx(pending_tx) → Txid
continue_pending_vhtlc_spend_txs() → ()
refund_expired_vhtlc(swap_id) → Txid
refund_expired_vhtlc_via_settlement(rng, swap_id) → Txid
offchain_balance() → OffChainBalance