Swap API
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:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to send |
receiving_currency | OrderbookCurrency | YES | Currency to receive |
amount | SwapAmount | YES | Amount to swap (sending or receiving) |
SwapAmount Object (one of):
| Field | Type | Description |
|---|---|---|
from | { amount: DecimalString } | Specify amount to send |
to | { amount: DecimalString } | Specify amount to receive |
Response:
| Field | Type | Description |
|---|---|---|
order_match | OrderMatch | Estimated 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:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to send |
receiving_currency | OrderbookCurrency | YES | Currency to receive |
amount | SwapAmount | YES | Amount to swap |
Response:
| Field | Type | Description |
|---|---|---|
order_id | string | Swap 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:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to send |
receiving_currency | OrderbookCurrency | YES | Currency to receive |
amount | SwapAmount | YES | Amount to swap |
price_change_tolerance | DecimalString | YES | Max price slippage (e.g., "0.01" = 1%) |
withdraw_sending_funds | bool | YES | Auto-withdraw sent funds after swap |
withdraw_receiving_funds | bool | YES | Auto-withdraw received funds after swap |
Response:
| Field | Type | Description |
|---|---|---|
estimate | SimpleSwapEstimate | Estimate details (one of multiple variants) |
SimpleSwapEstimate Variants:
Instant
Ready to swap immediately - you have sufficient offchain balance.
| Field | Type | Description |
|---|---|---|
order_match | OrderMatch | Expected execution |
sending_withdrawal_fee | WithdrawalFee | Fee to withdraw sent funds (optional) |
receiving_withdrawal_fee | WithdrawalFee | Fee to withdraw received funds (optional) |
Deferred
Need to set up channels via deposits / leases first.
| Field | Type | Description |
|---|---|---|
order_match | OrderMatch | Expected execution |
sending_channel_deposit | SendingChannelDeposit | Required deposit (optional) |
receiving_channel_lease | ReceivingChannelLease | Required lease (optional) |
sending_withdrawal_fee | WithdrawalFee | Withdrawal fee (optional) |
receiving_withdrawal_fee | WithdrawalFee | Withdrawal fee (optional) |
token_approval_fee | DecimalString | Native-asset fee for the token approval tx (optional, EVM only) |
DualFundDeferred
Need to dual-fund a channel.
| Field | Type | Description |
|---|---|---|
order_match | OrderMatch | Expected execution |
dual_fund_deposit | DualFundDeposit | Required dual-fund deposit |
sending_withdrawal_fee | WithdrawalFee | Withdrawal fee (optional) |
receiving_withdrawal_fee | WithdrawalFee | Withdrawal fee (optional) |
token_approval_fee | DecimalString | Native-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.
| Field | Type | Description |
|---|---|---|
sending_fee | DecimalString | Total fee in sending currency |
receiving_fee | DecimalString | Total fee in receiving currency |
LeaseTooBig
The receiving lease needed exceeds available liquidity or the provider's max capacity.
| Field | Type | Description |
|---|---|---|
needed_lease | DecimalString | Required lease amount |
available_lease_liquidity | DecimalString | Currently available |
max_lease_capacity | DecimalString | Provider's maximum |
NoLiquidity
No liquidity in the orderbook for this pair (empty payload).
Older versions of these docs documented a
NotEnoughBalanceandRentalTooBigvariant. Those names no longer exist — they were renamed toFeesHigherThanAmountandLeaseTooBigrespectively 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:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to send |
receiving_currency | OrderbookCurrency | YES | Currency to receive |
amount | SwapAmount | YES | Amount to swap |
price_change_tolerance | DecimalString | YES | Max price slippage |
withdraw_sending_funds | bool | YES | Auto-withdraw sent funds |
withdraw_receiving_funds | bool | YES | Auto-withdraw received funds |
Response:
| Field | Type | Description |
|---|---|---|
output | SimpleSwapOutput | Execution details |
SimpleSwapOutput:
| Field | Type | Description |
|---|---|---|
simple_swap_id | string | Unique 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 (
CancelOrderon the orderbook) instead.
Method: CancelSimpleSwap
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
simple_swap_id | string | YES | ID 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:
| Field | Type | Description |
|---|---|---|
timestamp | Timestamp | Update time |
simple_swap_id | string | Swap identifier |
update | - | Update type (one of many variants) |
Update Types:
| Update | Description |
|---|---|
funding_sending_channel | Opening/depositing sending channel |
leasing_receiving_channel | Renting receiving channel |
dual_funding_channel | Dual-funding channel |
sending_channel_ready | Sending channel active |
receiving_channel_ready | Receiving channel active |
dual_fund_channel_ready | Dual-funded channel active |
waiting_for_balances | Waiting for sufficient balance |
balances_ready | Balances sufficient |
order_created | Swap order placed |
order_completed | Swap executed |
withdrawing_sending_funds | Withdrawing sent funds |
withdrawing_receiving_funds | Withdrawing received funds |
withdrawing_dual_funded_funds | Withdrawing dual-fund |
sending_funds_withdrawn | Send withdrawal complete |
receiving_funds_withdrawn | Receive withdrawal complete |
dual_funded_funds_withdrawn | Dual-fund withdrawal complete |
simple_swap_completed | Swap fully complete |
simple_swap_error | Error 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
- Always estimate first - Call
EstimateSimpleSwapbeforeSimpleSwap - Set reasonable tolerance - 1-2% for most cases, higher for illiquid pairs
- Consider withdrawal costs - Withdrawing onchain has fees
- Monitor with streaming - Use
SubscribeSimpleSwapsfor real-time progress - Handle errors gracefully - Simple swaps can fail at multiple stages
- Check balance requirements - Ensure sufficient onchain balance for setup
- Use Simple Swap for beginners - Regular Swap requires channel management
Error Handling
| Error | Description | Solution |
|---|---|---|
NoLiquidity | No orderbook liquidity | Try different amount or wait |
FeesHigherThanAmount | Setup fees exceed swap amount | Increase swap amount, or pick a different pair |
LeaseTooBig | Receiving lease unavailable | Reduce amount or wait for provider liquidity |
FeesHigherThanAmount | Setup costs too high | Swap larger amount |
LeaseTooBig | Exceeds lease provider capacity | Reduce amount, wait for liquidity, or self-fund via Node API |
| Price exceeded tolerance | Market moved too much | Increase tolerance or retry |