Api

Swap API

Direct swaps and automated swaps

The Swap API provides cross-chain atomic swap functionality with automatic channel setup capabilities.

Endpoints


Estimate Swap

Estimate a swap between two currencies using existing offchain balance.

Method: EstimateSwap

Requirements:

  • Must have sufficient offchain balance for both sending and receiving currencies
  • Use Simple Swap if you don't have channels set up

Parameters:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to send
receiving_currencyOrderbookCurrencyYESCurrency to receive
amountSwapAmountYESAmount to swap (sending or receiving)

SwapAmount Object (one of):

FieldTypeDescription
from{ amount: DecimalString }Specify amount to send
to{ amount: DecimalString }Specify amount to receive

Response:

FieldTypeDescription
order_matchOrderMatchEstimated execution (optional - null if no liquidity)

Example Request:

TypeScript
import { SwapServiceClient, EstimateSwapRequest } from './proto/SwapServiceClientPb'

const client = new SwapServiceClient('http://localhost:5001')

const request = new EstimateSwapRequest()
request.setSendingCurrency({
  network: { protocol: 1, id: '0a03cf40' },
  assetId: 'BTC'
})
request.setReceivingCurrency({
  network: { protocol: 2, id: '11155111' },
  assetId: '0x0000000000000000000000000000000000000000'
})
request.setAmount({
  from: { amount: { value: '1000000' } } // 0.01 BTC
})

const response = await client.estimateSwap(request, {})
const match = response.getOrderMatch()

if (match) {
  console.log('Sending:', match.getSendingAmount())
  console.log('Receiving:', match.getReceivingAmount())
  console.log('Price:', match.getPrice())
} else {
  console.log('No liquidity available')
}
Go
import (
    "context"
    "log"
    pb "your-package/proto"
)

client := pb.NewSwapServiceClient(conn)

req := &pb.EstimateSwapRequest{
    SendingCurrency: &pb.OrderbookCurrency{
        Network: &pb.Network{
            Protocol: pb.Protocol_PROTOCOL_BITCOIN,
            Id:       "0a03cf40",
        },
        AssetId: "BTC",
    },
    ReceivingCurrency: &pb.OrderbookCurrency{
        Network: &pb.Network{
            Protocol: pb.Protocol_PROTOCOL_EVM,
            Id:       "11155111",
        },
        AssetId: "0x0000000000000000000000000000000000000000",
    },
    Amount: &pb.SwapAmount{
        Amount: &pb.SwapAmount_From_{
            From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: "0.01"}},
        },
    },
}

resp, err := client.EstimateSwap(context.Background(), req)
if err != nil {
    log.Fatalf("Error: %v", err)
}

if match := resp.GetOrderMatch(); match != nil {
    log.Printf("Sending: %s", match.GetSendingAmount())
    log.Printf("Receiving: %s", match.GetReceivingAmount())
    log.Printf("Price: %s", match.GetPrice())
} else {
    log.Println("No liquidity available")
}
Rust
use tonic::Request;
use your_package::swap_service_client::SwapServiceClient;
use your_package::{EstimateSwapRequest, OrderbookCurrency, Network, SwapAmount, DecimalString};

let mut client = SwapServiceClient::connect("http://localhost:5001").await?;

let request = Request::new(EstimateSwapRequest {
    sending_currency: Some(OrderbookCurrency {
        network: Some(Network {
            protocol: Protocol::Bitcoin as i32,
            id: "0a03cf40".to_string(),
        }),
        asset_id: "BTC".to_string(),
    }),
    receiving_currency: Some(OrderbookCurrency {
        network: Some(Network {
            protocol: Protocol::Evm as i32,
            id: "11155111".to_string(),
        }),
        asset_id: "0x0000000000000000000000000000000000000000".to_string(),
    }),
    amount: Some(SwapAmount {
        amount: Some(your_package::swap_amount::Amount::From(your_package::swap_amount::From {
            amount: Some(DecimalString { value: "0.01".to_string() }),
        })),
    }),
});

let response = client.estimate_swap(request).await?.into_inner();

if let Some(match_) = response.order_match {
    println!("Sending: {}", match_.sending_amount);
    println!("Receiving: {}", match_.receiving_amount);
    println!("Price: {}", match_.price);
} else {
    println!("No liquidity available");
}

Example Response:

{
  "order_match": {
    "sending_amount": "1000000",
    "receiving_amount": "28500000000000000000",
    "price": "28.5"
  }
}

Swap

Execute a swap between two currencies using existing offchain balance.

Method: Swap

Requirements:

  • Must have sufficient offchain balance for both sending and receiving currencies
  • Channels must be active for both currencies
  • Use Simple Swap for automatic channel setup

Parameters:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to send
receiving_currencyOrderbookCurrencyYESCurrency to receive
amountSwapAmountYESAmount to swap

Response:

FieldTypeDescription
order_idstringSwap order identifier

Example Request:

TypeScript
const request = new SwapRequest()
request.setSendingCurrency({
  network: { protocol: 1, id: '0a03cf40' },
  assetId: 'BTC'
})
request.setReceivingCurrency({
  network: { protocol: 2, id: '11155111' },
  assetId: '0x0000000000000000000000000000000000000000'
})
request.setAmount({
  from: { amount: { value: '1000000' } }
})

const response = await client.swap(request, {})
const orderId = response.getOrderId()
console.log('Swap order created:', orderId)
Go
req := &pb.SwapRequest{
    SendingCurrency: &pb.OrderbookCurrency{
        Network: &pb.Network{
            Protocol: pb.Protocol_PROTOCOL_BITCOIN,
            Id:       "0a03cf40",
        },
        AssetId: "BTC",
    },
    ReceivingCurrency: &pb.OrderbookCurrency{
        Network: &pb.Network{
            Protocol: pb.Protocol_PROTOCOL_EVM,
            Id:       "11155111",
        },
        AssetId: "0x0000000000000000000000000000000000000000",
    },
    Amount: &pb.SwapAmount{
        Amount: &pb.SwapAmount_From_{From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: "1000000"}}},
    },
}

resp, err := client.Swap(context.Background(), req)
if err != nil {
    log.Fatalf("Error: %v", err)
}

orderId := resp.GetOrderId()
log.Printf("Swap order created: %s", orderId)
Rust
let request = Request::new(SwapRequest {
    sending_currency: Some(OrderbookCurrency {
        network: Some(Network {
            protocol: Protocol::Bitcoin as i32,
            id: "0a03cf40".to_string(),
        }),
        asset_id: "BTC".to_string(),
    }),
    receiving_currency: Some(OrderbookCurrency {
        network: Some(Network {
            protocol: Protocol::Evm as i32,
            id: "11155111".to_string(),
        }),
        asset_id: "0x0000000000000000000000000000000000000000".to_string(),
    }),
    amount: Some(SwapAmount {
        amount: Some(your_package::swap_amount::Amount::From(swap_amount::From { amount: Some(DecimalString { value: "1000000".to_string() }) })),
    }),
});

let response = client.swap(request).await?.into_inner();
let order_id = response.order_id;
println!("Swap order created: {}", order_id);

Estimate Simple Swap

Estimate a swap with automatic channel setup. This calculates all fees including channel opening, deposits, and rentals.

Method: EstimateSimpleSwap

Benefits:

  • No need for existing channels
  • Automatic balance management
  • One-click cross-chain swaps
  • Optional auto-withdrawal

Parameters:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to send
receiving_currencyOrderbookCurrencyYESCurrency to receive
amountSwapAmountYESAmount to swap
price_change_toleranceDecimalStringYESMax price slippage (e.g., "0.01" = 1%)
withdraw_sending_fundsboolYESAuto-withdraw sent funds after swap
withdraw_receiving_fundsboolYESAuto-withdraw received funds after swap

Response:

FieldTypeDescription
estimateSimpleSwapEstimateEstimate details (one of multiple variants)

SimpleSwapEstimate Variants:

Instant

Ready to swap immediately - you have sufficient offchain balance.

FieldTypeDescription
order_matchOrderMatchExpected execution
sending_withdrawal_feeWithdrawalFeeFee to withdraw sent funds (optional)
receiving_withdrawal_feeWithdrawalFeeFee to withdraw received funds (optional)

Deferred

Need to set up channels via deposits / leases first.

FieldTypeDescription
order_matchOrderMatchExpected execution
sending_channel_depositSendingChannelDepositRequired deposit (optional)
receiving_channel_leaseReceivingChannelLeaseRequired lease (optional)
sending_withdrawal_feeWithdrawalFeeWithdrawal fee (optional)
receiving_withdrawal_feeWithdrawalFeeWithdrawal fee (optional)
token_approval_feeDecimalStringNative-asset fee for the token approval tx (optional, EVM only)

DualFundDeferred

Need to dual-fund a channel.

FieldTypeDescription
order_matchOrderMatchExpected execution
dual_fund_depositDualFundDepositRequired dual-fund deposit
sending_withdrawal_feeWithdrawalFeeWithdrawal fee (optional)
receiving_withdrawal_feeWithdrawalFeeWithdrawal fee (optional)
token_approval_feeDecimalStringNative-asset fee for the token approval tx (optional, EVM only)

FeesHigherThanAmount

The fees needed to set up channels would exceed the swap amount — the swap is uneconomical and won't be performed.

FieldTypeDescription
sending_feeDecimalStringTotal fee in sending currency
receiving_feeDecimalStringTotal fee in receiving currency

LeaseTooBig

The receiving lease needed exceeds available liquidity or the provider's max capacity.

FieldTypeDescription
needed_leaseDecimalStringRequired lease amount
available_lease_liquidityDecimalStringCurrently available
max_lease_capacityDecimalStringProvider's maximum

NoLiquidity

No liquidity in the orderbook for this pair (empty payload).

Older versions of these docs documented a NotEnoughBalance and RentalTooBig variant. Those names no longer exist — they were renamed to FeesHigherThanAmount and LeaseTooBig respectively when the rental flow was rewritten as the Lease flow.

Example Request:

import {
  EstimateSimpleSwapRequest,
} from './proto/swap_pb'

const request = new EstimateSimpleSwapRequest()
request.setSendingCurrency({
  protocol: 1, networkId: '0a03cf40', assetId: 'BTC'
})
request.setReceivingCurrency({
  protocol: 2, networkId: '11155111',
  assetId: '0x0000000000000000000000000000000000000000'
})
// Amounts are human-readable. "0.1" means 0.1 BTC, not 0.1 sats.
request.setAmount({ from: { amount: { value: '0.1' } } })
request.setPriceChangeTolerance({ value: '0.01' }) // 1 %
request.setWithdrawSendingFunds(false)
request.setWithdrawReceivingFunds(true)

const response = await swap.estimateSimpleSwap(request, {})
const estimate = response.getEstimate()

if (estimate.hasInstant()) {
  const instant = estimate.getInstant()
  console.log('Ready instantly. Will receive:',
    instant.getOrderMatch()?.getReceivingAmount()?.getValue())
}

if (estimate.hasDeferred()) {
  const deferred = estimate.getDeferred()
  if (deferred.hasSendingChannelDeposit()) {
    const dep = deferred.getSendingChannelDeposit()
    console.log('Sending-channel deposit:', dep?.getAmount()?.getValue(),
      'fee:', dep?.getFee()?.getValue())
  }
  if (deferred.hasReceivingChannelLease()) {
    const lease = deferred.getReceivingChannelLease()
    console.log('Receiving-channel lease:', lease?.getAmount()?.getValue(),
      'lease_fee:', lease?.getLeaseFee()?.getValue())
  }
}

if (estimate.hasDualFundDeferred()) {
  const dfd = estimate.getDualFundDeferred()
  console.log('Dual-fund deposit needed:',
    dfd?.getDualFundDeposit()?.getSelfAmount()?.getValue())
}

if (estimate.hasFeesHigherThanAmount()) {
  const fhta = estimate.getFeesHigherThanAmount()
  console.error('Fees exceed swap amount:',
    'sending fee', fhta?.getSendingFee()?.getValue(),
    'receiving fee', fhta?.getReceivingFee()?.getValue())
}

if (estimate.hasLeaseTooBig()) {
  const ltb = estimate.getLeaseTooBig()
  console.error('Lease unavailable:',
    'needed', ltb?.getNeededLease()?.getValue(),
    'available', ltb?.getAvailableLeaseLiquidity()?.getValue())
}

if (estimate.hasNoLiquidity()) {
  console.error('No market liquidity for this pair')
}

Example Response (Instant):

{
  "estimate": {
    "instant": {
      "order_match": {
        "sending_amount": "0.1",
        "receiving_amount": "285",
        "price": "2850"
      },
      "receiving_withdrawal_fee": {
        "fee": "0.0005",
        "fee_payment_currency": "FEE_PAYMENT_CURRENCY_RECEIVING"
      }
    }
  }
}

Example Response (Deferred):

{
  "estimate": {
    "deferred": {
      "order_match": {
        "sending_amount": "0.1",
        "receiving_amount": "285",
        "price": "2850"
      },
      "sending_channel_deposit": {
        "amount": "0.1",
        "unspendable_reserve": "0.00000546",
        "fee": "0.000025",
        "fee_payment_currency": "FEE_PAYMENT_CURRENCY_SENDING",
        "counterparty": "02abc123..."
      },
      "receiving_channel_lease": {
        "amount": "285",
        "unspendable_reserve": "0.001",
        "lease_fee": "0.3",
        "tx_fee_rate": {
          "max_fee_per_unit": "50",
          "priority_fee_per_unit": "5"
        },
        "pay_with_sending": true
      }
    }
  }
}

Simple Swap

Execute a swap with automatic channel setup.

Method: SimpleSwap

Features:

  • Automatically opens/deposits channels if needed
  • Rents inbound liquidity if required
  • Performs the swap when channels are ready
  • Optionally withdraws funds back onchain

Parameters:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to send
receiving_currencyOrderbookCurrencyYESCurrency to receive
amountSwapAmountYESAmount to swap
price_change_toleranceDecimalStringYESMax price slippage
withdraw_sending_fundsboolYESAuto-withdraw sent funds
withdraw_receiving_fundsboolYESAuto-withdraw received funds

Response:

FieldTypeDescription
outputSimpleSwapOutputExecution details

SimpleSwapOutput:

FieldTypeDescription
simple_swap_idstringUnique swap identifier
instant or deferred or dual_fund_deferred-Execution type

Example Request:

TypeScript
const request = new SimpleSwapRequest()
request.setSendingCurrency({
  network: { protocol: 1, id: '0a03cf40' },
  assetId: 'BTC'
})
request.setReceivingCurrency({
  network: { protocol: 2, id: '11155111' },
  assetId: '0x0000000000000000000000000000000000000000'
})
request.setAmount({
  from: { amount: { value: '10000000' } }
})
request.setPriceChangeTolerance('0.01')
request.setWithdrawSendingFunds(false)
request.setWithdrawReceivingFunds(true)

const response = await client.simpleSwap(request, {})
const output = response.getOutput()
const swapId = output.getSimpleSwapId()

console.log('Simple swap started:', swapId)

if (output.hasInstant()) {
  const instant = output.getInstant()
  console.log('Swap order:', instant.getOrderId())
}

if (output.hasDeferred()) {
  console.log('Setting up channels...')
  console.log('Subscribe to updates with SubscribeSimpleSwaps')
}
Go
req := &pb.SimpleSwapRequest{
    SendingCurrency: &pb.OrderbookCurrency{
        Network: &pb.Network{
            Protocol: pb.Protocol_PROTOCOL_BITCOIN,
            Id:       "0a03cf40",
        },
        AssetId: "BTC",
    },
    ReceivingCurrency: &pb.OrderbookCurrency{
        Network: &pb.Network{
            Protocol: pb.Protocol_PROTOCOL_EVM,
            Id:       "11155111",
        },
        AssetId: "0x0000000000000000000000000000000000000000",
    },
    Amount: &pb.SwapAmount{
        Amount: &pb.SwapAmount_From_{From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: "10000000"}}},
    },
    PriceChangeTolerance:   "0.01",
    WithdrawSendingFunds:   false,
    WithdrawReceivingFunds: true,
}

resp, err := client.SimpleSwap(context.Background(), req)
if err != nil {
    log.Fatalf("Error: %v", err)
}

output := resp.GetOutput()
swapId := output.GetSimpleSwapId()

log.Printf("Simple swap started: %s", swapId)

if instant := output.GetInstant(); instant != nil {
    log.Printf("Swap order: %s", instant.GetOrderId())
}

if output.GetDeferred() != nil {
    log.Println("Setting up channels...")
    log.Println("Subscribe to updates with SubscribeSimpleSwaps")
}
Rust
let request = Request::new(SimpleSwapRequest {
    sending_currency: Some(OrderbookCurrency {
        network: Some(Network {
            protocol: Protocol::Bitcoin as i32,
            id: "0a03cf40".to_string(),
        }),
        asset_id: "BTC".to_string(),
    }),
    receiving_currency: Some(OrderbookCurrency {
        network: Some(Network {
            protocol: Protocol::Evm as i32,
            id: "11155111".to_string(),
        }),
        asset_id: "0x0000000000000000000000000000000000000000".to_string(),
    }),
    amount: Some(SwapAmount {
        amount: Some(your_package::swap_amount::Amount::From(swap_amount::From { amount: Some(DecimalString { value: "10000000".to_string() }) })),
    }),
    price_change_tolerance: "0.01".to_string(),
    withdraw_sending_funds: false,
    withdraw_receiving_funds: true,
});

let response = client.simple_swap(request).await?.into_inner();
let output = response.output.unwrap();
let swap_id = output.simple_swap_id;

println!("Simple swap started: {}", swap_id);

match output.output {
    Some(Output::Instant(instant)) => {
        println!("Swap order: {}", instant.order_id);
    }
    Some(Output::Deferred(_)) => {
        println!("Setting up channels...");
        println!("Subscribe to updates with SubscribeSimpleSwaps");
    }
    _ => {}
}

Cancel Simple Swap

Cancels an ongoing simple swap operation. Useful when channel setup is taking too long, the user changes their mind, or the orderbook moves outside the configured price tolerance.

Only valid while the simple swap is still in setup. Once the underlying order has been created, use the standard order-cancellation flow (CancelOrder on the orderbook) instead.

Method: CancelSimpleSwap

Parameters:

NameTypeRequiredDescription
simple_swap_idstringYESID returned from SimpleSwap

Response: Empty.

Example Request:

import { CancelSimpleSwapRequest } from './proto/swap_pb'

const request = new CancelSimpleSwapRequest()
request.setSimpleSwapId('ss_abc123')

await client.cancelSimpleSwap(request, {})
console.log('Simple swap cancelled')

Example Response:

{}

Subscribe Simple Swaps

Subscribe to real-time updates for all simple swap operations.

Method: SubscribeSimpleSwaps

Parameters: None

Response: Stream of SimpleSwapUpdate

SimpleSwapUpdate:

FieldTypeDescription
timestampTimestampUpdate time
simple_swap_idstringSwap identifier
update-Update type (one of many variants)

Update Types:

UpdateDescription
funding_sending_channelOpening/depositing sending channel
leasing_receiving_channelRenting receiving channel
dual_funding_channelDual-funding channel
sending_channel_readySending channel active
receiving_channel_readyReceiving channel active
dual_fund_channel_readyDual-funded channel active
waiting_for_balancesWaiting for sufficient balance
balances_readyBalances sufficient
order_createdSwap order placed
order_completedSwap executed
withdrawing_sending_fundsWithdrawing sent funds
withdrawing_receiving_fundsWithdrawing received funds
withdrawing_dual_funded_fundsWithdrawing dual-fund
sending_funds_withdrawnSend withdrawal complete
receiving_funds_withdrawnReceive withdrawal complete
dual_funded_funds_withdrawnDual-fund withdrawal complete
simple_swap_completedSwap fully complete
simple_swap_errorError occurred

Example:

TypeScript
const request = new SubscribeSimpleSwapsRequest()
const stream = client.subscribeSimpleSwaps(request, {})

stream.on('data', (update) => {
  const timestamp = update.getTimestamp()
  const swapId = update.getSimpleSwapId()

  if (update.hasFundingSendingChannel()) {
    const funding = update.getFundingSendingChannel()
    console.log(`[${swapId}] Funding channel ${funding.getChannelId()}`)
    console.log(`  TX: ${funding.getTxid()}`)
    console.log(`  Amount: ${funding.getAmount()}`)
  }

  if (update.hasLeasingReceivingChannel()) {
    const renting = update.getLeasingReceivingChannel()
    console.log(`[${swapId}] Renting channel ${renting.getChannelId()}`)
    console.log(`  TX: ${renting.getTxid()}`)
  }

  if (update.hasSendingChannelReady()) {
    console.log(`[${swapId}] Sending channel ready`)
  }

  if (update.hasReceivingChannelReady()) {
    console.log(`[${swapId}] Receiving channel ready`)
  }

  if (update.hasWaitingForBalances()) {
    const waiting = update.getWaitingForBalances()
    console.log(`[${swapId}] Waiting for balances...`)
    console.log(`  Need sending: ${waiting.getNeededSending()}`)
    console.log(`  Need receiving: ${waiting.getNeededReceiving()}`)
  }

  if (update.hasBalancesReady()) {
    console.log(`[${swapId}] Balances ready!`)
  }

  if (update.hasOrderCreated()) {
    const order = update.getOrderCreated()
    console.log(`[${swapId}] Order created: ${order.getOrderId()}`)
  }

  if (update.hasOrderCompleted()) {
    const completed = update.getOrderCompleted()
    console.log(`[${swapId}] Order completed!`)
    console.log(`  Sent: ${completed.getSentAmount()}`)
    console.log(`  Received: ${completed.getReceivedAmount()}`)
  }

  if (update.hasWithdrawingReceivingFunds()) {
    const withdrawing = update.getWithdrawingReceivingFunds()
    console.log(`[${swapId}] Withdrawing funds...`)
    console.log(`  TXs: ${withdrawing.getTxidsList()}`)
  }

  if (update.hasSimpleSwapCompleted()) {
    console.log(`[${swapId}] ✓ Swap completed successfully!`)
  }

  if (update.hasSimpleSwapError()) {
    const error = update.getSimpleSwapError()
    console.error(`[${swapId}] ✗ Error: ${error.getError()}`)
  }
})

stream.on('error', (err) => console.error('Stream error:', err))
Go
req := &pb.SubscribeSimpleSwapsRequest{}
stream, err := client.SubscribeSimpleSwaps(context.Background(), req)
if err != nil {
    log.Fatalf("Error: %v", err)
}

for {
    update, err := stream.Recv()
    if err != nil {
        log.Printf("Stream error: %v", err)
        break
    }

    swapId := update.GetSimpleSwapId()

    if funding := update.GetFundingSendingChannel(); funding != nil {
        log.Printf("[%s] Funding channel %s", swapId, funding.GetChannelId())
        log.Printf("  TX: %s", funding.GetTxid())
        log.Printf("  Amount: %s", funding.GetAmount())
    }

    if renting := update.GetLeasingReceivingChannel(); renting != nil {
        log.Printf("[%s] Renting channel %s", swapId, renting.GetChannelId())
        log.Printf("  TX: %s", renting.GetTxid())
    }

    if update.GetSendingChannelReady() != nil {
        log.Printf("[%s] Sending channel ready", swapId)
    }

    if update.GetReceivingChannelReady() != nil {
        log.Printf("[%s] Receiving channel ready", swapId)
    }

    if waiting := update.GetWaitingForBalances(); waiting != nil {
        log.Printf("[%s] Waiting for balances...", swapId)
        log.Printf("  Need sending: %s", waiting.GetNeededSending())
        log.Printf("  Need receiving: %s", waiting.GetNeededReceiving())
    }

    if update.GetBalancesReady() != nil {
        log.Printf("[%s] Balances ready!", swapId)
    }

    if order := update.GetOrderCreated(); order != nil {
        log.Printf("[%s] Order created: %s", swapId, order.GetOrderId())
    }

    if completed := update.GetOrderCompleted(); completed != nil {
        log.Printf("[%s] Order completed!", swapId)
        log.Printf("  Sent: %s", completed.GetSentAmount())
        log.Printf("  Received: %s", completed.GetReceivedAmount())
    }

    if withdrawing := update.GetWithdrawingReceivingFunds(); withdrawing != nil {
        log.Printf("[%s] Withdrawing funds...", swapId)
        log.Printf("  TXs: %v", withdrawing.GetTxids())
    }

    if update.GetSimpleSwapCompleted() != nil {
        log.Printf("[%s] Swap completed successfully!", swapId)
    }

    if swapError := update.GetSimpleSwapError(); swapError != nil {
        log.Printf("[%s] Error: %s", swapId, swapError.GetError())
    }
}
Rust
let request = Request::new(SubscribeSimpleSwapsRequest {});
let mut stream = client.subscribe_simple_swaps(request).await?.into_inner();

while let Some(update) = stream.message().await? {
    let swap_id = &update.simple_swap_id;

    if let Some(funding) = &update.funding_sending_channel {
        println!("[{}] Funding channel {}", swap_id, funding.channel_id);
        println!("  TX: {}", funding.txid);
        println!("  Amount: {}", funding.amount);
    }

    if let Some(renting) = &update.leasing_receiving_channel {
        println!("[{}] Renting channel {}", swap_id, renting.channel_id);
        println!("  TX: {}", renting.txid);
    }

    if update.sending_channel_ready.is_some() {
        println!("[{}] Sending channel ready", swap_id);
    }

    if update.receiving_channel_ready.is_some() {
        println!("[{}] Receiving channel ready", swap_id);
    }

    if let Some(waiting) = &update.waiting_for_balances {
        println!("[{}] Waiting for balances...", swap_id);
        println!("  Need sending: {}", waiting.needed_sending);
        println!("  Need receiving: {}", waiting.needed_receiving);
    }

    if update.balances_ready.is_some() {
        println!("[{}] Balances ready!", swap_id);
    }

    if let Some(order) = &update.order_created {
        println!("[{}] Order created: {}", swap_id, order.order_id);
    }

    if let Some(completed) = &update.order_completed {
        println!("[{}] Order completed!", swap_id);
        println!("  Sent: {}", completed.sent_amount);
        println!("  Received: {}", completed.received_amount);
    }

    if let Some(withdrawing) = &update.withdrawing_receiving_funds {
        println!("[{}] Withdrawing funds...", swap_id);
        println!("  TXs: {:?}", withdrawing.txids);
    }

    if update.simple_swap_completed.is_some() {
        println!("[{}] Swap completed successfully!", swap_id);
    }

    if let Some(error) = &update.simple_swap_error {
        eprintln!("[{}] Error: {}", swap_id, error.error);
    }
}

Common Workflows

One-click cross-chain swap

TypeScript
async function oneClickSwap(
  client: SwapServiceClient,
  fromCurrency: OrderbookCurrency,
  toCurrency: OrderbookCurrency,
  amount: string,
  withdrawToWallet: boolean = true
) {
  // 1. Estimate
  const estimateReq = new EstimateSimpleSwapRequest()
  estimateReq.setSendingCurrency(fromCurrency)
  estimateReq.setReceivingCurrency(toCurrency)
  estimateReq.setAmount({ sending: { value: amount } })
  estimateReq.setPriceChangeTolerance('0.02') // 2% max slippage
  estimateReq.setWithdrawSendingFunds(false)
  estimateReq.setWithdrawReceivingFunds(withdrawToWallet)

  const estimate = await client.estimateSimpleSwap(estimateReq, {})

  if (estimate.getEstimate().hasNoLiquidity()) {
    throw new Error('No liquidity available')
  }
  if (estimate.getEstimate().hasFeesHigherThanAmount()) {
    throw new Error('Setup fees would exceed the swap amount')
  }
  if (estimate.getEstimate().hasLeaseTooBig()) {
    const ltb = estimate.getEstimate().getLeaseTooBig()
    throw new Error(
      `Lease unavailable — needed ${ltb?.getNeededLease()?.getValue()}, ` +
      `have ${ltb?.getAvailableLeaseLiquidity()?.getValue()}`
    )
  }

  // 2. Execute
  const swapReq = new SimpleSwapRequest()
  swapReq.setSendingCurrency(fromCurrency)
  swapReq.setReceivingCurrency(toCurrency)
  swapReq.setAmount({ sending: { value: amount } })
  swapReq.setPriceChangeTolerance('0.02')
  swapReq.setWithdrawSendingFunds(false)
  swapReq.setWithdrawReceivingFunds(withdrawToWallet)

  const response = await client.simpleSwap(swapReq, {})
  return response.getOutput().getSimpleSwapId()
}

// Example usage
const swapId = await oneClickSwap(
  client,
  btcCurrency,
  ethCurrency,
  '10000000', // 0.1 BTC
  true // Auto-withdraw ETH to wallet
)

console.log('Swap started:', swapId)
console.log('Monitor progress with SubscribeSimpleSwaps')
Go
func oneClickSwap(
    client pb.SwapServiceClient,
    fromCurrency *pb.OrderbookCurrency,
    toCurrency *pb.OrderbookCurrency,
    amount string,
    withdrawToWallet bool,
) (string, error) {
    // 1. Estimate
    estimateReq := &pb.EstimateSimpleSwapRequest{
        SendingCurrency:   fromCurrency,
        ReceivingCurrency: toCurrency,
        Amount: &pb.SwapAmount{
            Amount: &pb.SwapAmount_From_{From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: amount}}},
        },
        PriceChangeTolerance:   "0.02", // 2% max slippage
        WithdrawSendingFunds:   false,
        WithdrawReceivingFunds: withdrawToWallet,
    }

    estimate, err := client.EstimateSimpleSwap(context.Background(), estimateReq)
    if err != nil {
        return "", err
    }

    if estimate.GetEstimate().GetNoLiquidity() != nil {
        return "", fmt.Errorf("no liquidity available")
    }
    if estimate.GetEstimate().GetFeesHigherThanAmount() != nil {
        return "", fmt.Errorf("setup fees would exceed the swap amount")
    }
    if ltb := estimate.GetEstimate().GetLeaseTooBig(); ltb != nil {
        return "", fmt.Errorf("lease unavailable — needed %s, have %s",
            ltb.GetNeededLease().GetValue(),
            ltb.GetAvailableLeaseLiquidity().GetValue())
    }

    // 2. Execute
    swapReq := &pb.SimpleSwapRequest{
        SendingCurrency:        fromCurrency,
        ReceivingCurrency:      toCurrency,
        Amount: &pb.SwapAmount{
            Amount: &pb.SwapAmount_From_{From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: amount}}},
        },
        PriceChangeTolerance:   "0.02",
        WithdrawSendingFunds:   false,
        WithdrawReceivingFunds: withdrawToWallet,
    }

    response, err := client.SimpleSwap(context.Background(), swapReq)
    if err != nil {
        return "", err
    }

    return response.GetOutput().GetSimpleSwapId(), nil
}

// Example usage
swapId, err := oneClickSwap(
    client,
    btcCurrency,
    ethCurrency,
    "10000000", // 0.1 BTC
    true,       // Auto-withdraw ETH to wallet
)
if err != nil {
    log.Fatalf("Error: %v", err)
}

log.Printf("Swap started: %s", swapId)
log.Println("Monitor progress with SubscribeSimpleSwaps")
Rust
async fn one_click_swap(
    client: &mut SwapServiceClient<Channel>,
    from_currency: OrderbookCurrency,
    to_currency: OrderbookCurrency,
    amount: String,
    withdraw_to_wallet: bool,
) -> Result<String, Box<dyn std::error::Error>> {
    // 1. Estimate
    let estimate_req = Request::new(EstimateSimpleSwapRequest {
        sending_currency: Some(from_currency.clone()),
        receiving_currency: Some(to_currency.clone()),
        amount: Some(SwapAmount {
            amount: Some(your_package::swap_amount::Amount::From(swap_amount::From { amount: Some(DecimalString { value: amount.clone().to_string() }) })),
        }),
        price_change_tolerance: "0.02".to_string(), // 2% max slippage
        withdraw_sending_funds: false,
        withdraw_receiving_funds: withdraw_to_wallet,
    });

    let estimate = client.estimate_simple_swap(estimate_req).await?.into_inner();

    match &estimate.estimate.as_ref().unwrap().estimate {
        Some(Estimate::NoLiquidity(_)) => {
            return Err("No liquidity available".into());
        }
        Some(Estimate::FeesHigherThanAmount(_)) => {
            return Err("Setup fees would exceed the swap amount".into());
        }
        Some(Estimate::LeaseTooBig(ltb)) => {
            return Err(format!(
                "Lease unavailable — needed {}, have {}",
                ltb.needed_lease.as_ref().unwrap().value,
                ltb.available_lease_liquidity.as_ref().unwrap().value
            ).into());
        }
        _ => {}
    }

    // 2. Execute
    let swap_req = Request::new(SimpleSwapRequest {
        sending_currency: Some(from_currency),
        receiving_currency: Some(to_currency),
        amount: Some(SwapAmount {
            amount: Some(your_package::swap_amount::Amount::From(swap_amount::From { amount: Some(DecimalString { value: amount.to_string() }) })),
        }),
        price_change_tolerance: "0.02".to_string(),
        withdraw_sending_funds: false,
        withdraw_receiving_funds: withdraw_to_wallet,
    });

    let response = client.simple_swap(swap_req).await?.into_inner();
    Ok(response.output.unwrap().simple_swap_id)
}

// Example usage
let swap_id = one_click_swap(
    &mut client,
    btc_currency,
    eth_currency,
    "10000000".to_string(), // 0.1 BTC
    true,                    // Auto-withdraw ETH to wallet
).await?;

println!("Swap started: {}", swap_id);
println!("Monitor progress with SubscribeSimpleSwaps");

Monitor swap progress

TypeScript
async function waitForSwapCompletion(
  client: SwapServiceClient,
  swapId: string
): Promise<void> {
  return new Promise((resolve, reject) => {
    const stream = client.subscribeSimpleSwaps(new SubscribeSimpleSwapsRequest(), {})

    stream.on('data', (update) => {
      if (update.getSimpleSwapId() !== swapId) return

      if (update.hasSimpleSwapCompleted()) {
        stream.cancel()
        resolve()
      }

      if (update.hasSimpleSwapError()) {
        stream.cancel()
        reject(new Error(update.getSimpleSwapError()?.getError()))
      }
    })

    stream.on('error', (err) => reject(err))
  })
}

// Usage
try {
  await waitForSwapCompletion(client, swapId)
  console.log('Swap completed successfully!')
} catch (error) {
  console.error('Swap failed:', error.message)
}
Go
func waitForSwapCompletion(
    client pb.SwapServiceClient,
    swapId string,
) error {
    req := &pb.SubscribeSimpleSwapsRequest{}
    stream, err := client.SubscribeSimpleSwaps(context.Background(), req)
    if err != nil {
        return err
    }

    for {
        update, err := stream.Recv()
        if err != nil {
            return err
        }

        if update.GetSimpleSwapId() != swapId {
            continue
        }

        if update.GetSimpleSwapCompleted() != nil {
            return nil
        }

        if swapError := update.GetSimpleSwapError(); swapError != nil {
            return fmt.Errorf("swap error: %s", swapError.GetError())
        }
    }
}

// Usage
err := waitForSwapCompletion(client, swapId)
if err != nil {
    log.Printf("Swap failed: %v", err)
} else {
    log.Println("Swap completed successfully!")
}
Rust
async fn wait_for_swap_completion(
    client: &mut SwapServiceClient<Channel>,
    swap_id: String,
) -> Result<(), Box<dyn std::error::Error>> {
    let request = Request::new(SubscribeSimpleSwapsRequest {});
    let mut stream = client.subscribe_simple_swaps(request).await?.into_inner();

    while let Some(update) = stream.message().await? {
        if update.simple_swap_id != swap_id {
            continue;
        }

        if update.simple_swap_completed.is_some() {
            return Ok(());
        }

        if let Some(error) = update.simple_swap_error {
            return Err(format!("Swap error: {}", error.error).into());
        }
    }

    Err("Stream ended unexpectedly".into())
}

// Usage
match wait_for_swap_completion(&mut client, swap_id).await {
    Ok(_) => println!("Swap completed successfully!"),
    Err(e) => eprintln!("Swap failed: {}", e),
}

Price Change Tolerance

The price_change_tolerance parameter protects you from excessive slippage:

TypeScript
// Formula: (max_tolerated_price - intended_price) / intended_price

// Example: BTC/ETH at 28.5 ETH per BTC
// With 1% tolerance (0.01):
// - Max buy price: 28.5 * 1.01 = 28.785 ETH/BTC
// - Min sell price: 28.5 * 0.99 = 28.215 ETH/BTC

request.setPriceChangeTolerance('0.01') // 1% tolerance
Go
// Formula: (max_tolerated_price - intended_price) / intended_price

// Example: BTC/ETH at 28.5 ETH per BTC
// With 1% tolerance (0.01):
// - Max buy price: 28.5 * 1.01 = 28.785 ETH/BTC
// - Min sell price: 28.5 * 0.99 = 28.215 ETH/BTC

request.PriceChangeTolerance = "0.01" // 1% tolerance
Rust
// Formula: (max_tolerated_price - intended_price) / intended_price

// Example: BTC/ETH at 28.5 ETH per BTC
// With 1% tolerance (0.01):
// - Max buy price: 28.5 * 1.01 = 28.785 ETH/BTC
// - Min sell price: 28.5 * 0.99 = 28.215 ETH/BTC

request.price_change_tolerance = "0.01".to_string(); // 1% tolerance

Recommended values:

  • 0.005 (0.5%) - Very tight, may fail in volatile markets
  • 0.01 (1%) - Standard for most swaps
  • 0.02 (2%) - More lenient for illiquid pairs
  • 0.05 (5%) - Very lenient, use with caution

Best Practices

  1. Always estimate first - Call EstimateSimpleSwap before SimpleSwap
  2. Set reasonable tolerance - 1-2% for most cases, higher for illiquid pairs
  3. Consider withdrawal costs - Withdrawing onchain has fees
  4. Monitor with streaming - Use SubscribeSimpleSwaps for real-time progress
  5. Handle errors gracefully - Simple swaps can fail at multiple stages
  6. Check balance requirements - Ensure sufficient onchain balance for setup
  7. Use Simple Swap for beginners - Regular Swap requires channel management

Error Handling

ErrorDescriptionSolution
NoLiquidityNo orderbook liquidityTry different amount or wait
FeesHigherThanAmountSetup fees exceed swap amountIncrease swap amount, or pick a different pair
LeaseTooBigReceiving lease unavailableReduce amount or wait for provider liquidity
FeesHigherThanAmountSetup costs too highSwap larger amount
LeaseTooBigExceeds lease provider capacityReduce amount, wait for liquidity, or self-fund via Node API
Price exceeded toleranceMarket moved too muchIncrease tolerance or retry

← Back to API Reference | Next: Node API →


Copyright © 2025