Receiving an on-chain transaction (Swap-In)

There are cases when you have funds in some bitcoin address and you would like to send those to your lightning node.

In such cases, the SDK might have to open a new channel, for which case you can specify an optional user-selected channel opening fee1. In order to receive funds you first have to be connected to an LSP.

Rust
let swap_info = sdk
    .receive_onchain(ReceiveOnchainRequest::default())
    .await?;

// Send your funds to the below bitcoin address
let address = swap_info.bitcoin_address;
info!(
    "Minimum amount allowed to deposit in sats: {}",
    swap_info.min_allowed_deposit
);
info!(
    "Maximum amount allowed to deposit in sats: {}",
    swap_info.max_allowed_deposit
);
Swift
let swapInfo = try? sdk.receiveOnchain(req: ReceiveOnchainRequest())

// Send your funds to the bellow bitcoin address
let address = swapInfo?.bitcoinAddress
print("Minimum amount allowed to deposit in sats: \(swapInfo!.minAllowedDeposit)")
print("Maximum amount allowed to deposit in sats: \(swapInfo!.maxAllowedDeposit)")
Kotlin
try {
    val swapInfo = sdk.receiveOnchain(ReceiveOnchainRequest())
    // Send your funds to the bellow bitcoin address
    val address = swapInfo.bitcoinAddress
    // Log.v("Breez", "Minimum amount allowed to deposit in sats: ${swapInfo.minAllowedDeposit}")
    // Log.v("Breez", "Maximum amount allowed to deposit in sats: ${swapInfo.maxAllowedDeposit}")            
} catch (e: Exception) {
    // handle error
}
React Native
try {
  const swapInfo = await receiveOnchain({})

  // Send your funds to the below bitcoin address
  const address = swapInfo.bitcoinAddress
  console.log(`Minimum amount allowed to deposit in sats: ${swapInfo.minAllowedDeposit}`)
  console.log(`Maximum amount allowed to deposit in sats: ${swapInfo.maxAllowedDeposit}`)
} catch (err) {
  console.error(err)
}
Dart
ReceiveOnchainRequest req = const ReceiveOnchainRequest();
SwapInfo swapInfo = await BreezSDK().receiveOnchain(req: req);

// Send your funds to the below bitcoin address
String address = swapInfo.bitcoinAddress;
print(address);
print("Minimum amount allowed to deposit in sats: ${swapInfo.minAllowedDeposit}");
print("Maximum amount allowed to deposit in sats: ${swapInfo.maxAllowedDeposit}");
return swapInfo;
Python
swap_info = sdk_services.receive_onchain(breez_sdk.ReceiveOnchainRequest())

# Send your funds to the below bitcoin address
address = swap_info.bitcoin_address
print("Minimum amount allowed to deposit in sats:", swap_info.min_allowed_deposit)
print("Maximum amount allowed to deposit in sats:", swap_info.max_allowed_deposit)
Go
if swapInfo, err := sdk.ReceiveOnchain(breez_sdk.ReceiveOnchainRequest{}); err != nil {
    // Send your funds to the below bitcoin address
    address := swapInfo.BitcoinAddress
    log.Printf("%v", address)

    log.Printf("Minimum amount allowed to deposit in sats: %v", swapInfo.MinAllowedDeposit)
    log.Printf("Maximum amount allowed to deposit in sats: %v", swapInfo.MaxAllowedDeposit)
}
C#
try
{
    var swapInfo = sdk.ReceiveOnchain(new ReceiveOnchainRequest());
    // Send your funds to the below bitcoin address
    var address = swapInfo.bitcoinAddress;
    Console.WriteLine($"Minimum amount allowed to deposit in sats: {swapInfo.minAllowedDeposit}");
    Console.WriteLine($"Maximum amount allowed to deposit in sats: {swapInfo.maxAllowedDeposit}");
}
catch (Exception)
{
    // Handle error
}

Developer note

The swap_info above includes maximum and minimum limits. Your application's users must be informed of these limits because if the amount transferred to the swap address falls outside this valid range, the funds will not be successfully received via lightning. In such cases, a refund will be necessary.

Get the in-progress Swap

Once you've sent the funds to the above address, the SDK will monitor this address for unspent confirmed outputs and use a trustless submarine swap to receive these into your Lightning node. You can always monitor the status of the current in-progress swap using the following code:

Rust
let swap_info = sdk.in_progress_swap().await?;
Swift
let swapInfo = try? sdk.inProgressSwap()
Kotlin
try {
    val swapInfo = sdk.inProgressSwap()
} catch (e: Exception) {
    // handle error
}
React Native
try {
  const swapInfo = await inProgressSwap()
} catch (err) {
  console.error(err)
}
Dart
SwapInfo? swapInfo = await BreezSDK().inProgressSwap();
print(swapInfo);
Python
swap_info = sdk_services.in_progress_swap()
Go

if swapInfo, err := sdk.InProgressSwap(); err == nil {
    log.Printf("%#v", swapInfo)
}
C#
try
{
    var swapInfo = sdk.InProgressSwap();
}
catch (Exception)
{
    // Handle error
}

The process of receiving funds via an on-chain address is trustless and uses a submarine swap. This means there are two ways to spend the sent funds:

  1. Either by a preimage that is exposed when the Lightning payment is completed - this is the positive case where the swap was successful.
  2. Or by your node when the swap didn't complete within a certain timeout (216 blocks) - this is the negative case where your node will execute a refund (funds become refundable after 288 blocks). Refund will also be available in case the amount sent wasn't within the limits.

Notifications

Enabling mobile notifications will register your app for swap notifications.

This means that, when the user performs a swap-in (receive onchain), the app will

  • automatically complete the swap in the background when the onchain transaction is confirmed, even if the app is closed
  • display an OS notification, informing the user of the received funds

Refund a Swap

In order to execute a refund, you need to supply an on-chain address to where the refunded amount will be sent. The following code will retrieve the refundable swaps:

Rust
let refundables = sdk.list_refundables().await?;
Swift
let refundables = try? sdk.listRefundables()
Kotlin
try {
    val refundables = sdk.listRefundables()
} catch (e: Exception) {
    // handle error
}
React Native
try {
  const refundables = await listRefundables()
} catch (err) {
  console.error(err)
}
Dart
List<SwapInfo> refundables = await BreezSDK().listRefundables();
for (var refundable in refundables) {
  print(refundable.bitcoinAddress);
}
Python
refundables = sdk_services.list_refundables()
Go
if refundables, err := sdk.ListRefundables(); err == nil {
    log.Printf("%#v", refundables)
}
C#
try
{
    var refundables = sdk.ListRefundables();
}
catch (Exception)
{
    // Handle error
}

Once you have a refundable swap in hand, use the following code to execute a refund:

Rust
let destination_address = "...".into();
let sat_per_vbyte = refund_tx_fee_rate;

sdk.refund(RefundRequest {
    to_address: destination_address,
    sat_per_vbyte,
    swap_address: refundable.bitcoin_address,
})
.await?;
Swift
let destinationAddress = "..."
let response = try? sdk.refund(req: RefundRequest(swapAddress: refundables.bitcoinAddress, toAddress: destinationAddress, satPerVbyte: satPerVbyte))
Kotlin
val swapAddress = "..."
val toAddress = "..."
val satPerVbyte = 1.toUInt()
try {
    sdk.refund(RefundRequest(swapAddress, toAddress, satPerVbyte))
} catch (e: Exception) {
    // handle error
}
React Native
try {
  const refundables = await listRefundables()
  const toAddress = '...'
  const satPerVbyte = 5

  const refundResponse = await refund({
    swapAddress: refundables[0].bitcoinAddress,
    toAddress,
    satPerVbyte
  })
} catch (err) {
  console.error(err)
}
Dart
RefundRequest req = RefundRequest(
  swapAddress: swapAddress,
  toAddress: toAddress,
  satPerVbyte: satPerVbyte,
);
RefundResponse resp = await BreezSDK().refund(req: req);
print(resp.refundTxId);
Python
destination_address = "..."

sat_per_vbyte = 5
try:
    result = sdk_services.refund(
       swap_address=refundable.bitcoin_address,                         
       to_address=destination_address,
       sat_per_vbyte=sat_per_vbyte)
Go
if refundables, err := sdk.ListRefundables(); err == nil {
    destinationAddress := "..."
    satPerVbyte := uint32(5)
    refundRequest := breez_sdk.RefundRequest{
        SwapAddress: refundables[0].BitcoinAddress,
        ToAddress:   destinationAddress,
        SatPerVbyte: satPerVbyte,
    }
    if result, err := sdk.Refund(refundRequest); err == nil {
        log.Printf("%v", result)
    }
}
C#
var destinationAddress = "...";
var satPerVbyte = refundTxFeeRate;
try
{
    var result = sdk.Refund(
        new RefundRequest(
            refundable.bitcoinAddress,
            destinationAddress,
            satPerVbyte));
}
catch (Exception)
{
    // Handle error
}

Developer note

A refund can be attempted several times. A common scenario where this is useful is if the initial refund transaction takes too long to mine, your application's users can be offered the ability to re-trigger the refund with a higher feerate.

Rescanning swaps

The SDK continuously monitors any ongoing swap transactions until they are either completed or refunded. Once one of these outcomes occurs, the SDK ceases its monitoring activities, and users are advised against sending additional funds to the swap address. However, if users inadvertently send additional funds to a swap address that was already used, the SDK won't automatically recognize it. In such cases, the SDK provides an option to manually scan the used swap addressed to identify additional transactions. This action allows the address to be included in the list eligible for refunds, enabling the initiation of a refund process. For the purpose of rescanning all historical swap addresses and updating their on-chain status, the following code can be used:

Rust
sdk.rescan_swaps().await?;
Swift
try? sdk.rescanSwaps()
Kotlin
try {
    sdk.rescanSwaps()
} catch (e: Exception) {
    // handle error
}
React Native
try {
  await rescanSwaps()
} catch (err) {
  console.error(err)
}
Dart
await BreezSDK().rescanSwaps();  
Python
sdk_services.rescan_swaps()
Go
if err := sdk.RescanSwaps(); err == nil {
    log.Println("Rescan finished")
}
C#
try
{
    sdk.RescanSwaps();
}
catch (Exception)
{
    // Handle error
}

Calculating fees

When the amount to be received exceeds the inbound liquidity of the node, a new channel will be opened by the LSP in order for the node to receive it. This can checked by retrieving the NodeState from the SDK and comparing the inbound liquidity to the amount to be received. If the amount is greater or equal to the inbound liquidity, a new channel opening is required.

To calculate the fees for a channel being opened by the LSP:

Rust
let channel_fees = sdk
    .open_channel_fee(OpenChannelFeeRequest {
        amount_msat,
        expiry: None,
    })
    .await?;
Swift
let response = try? sdk.openChannelFee(req: OpenChannelFeeRequest(amountMsat: amountMsat))
Kotlin
try {
    val amountMsat = 5_000.toULong();
    val channelFees = sdk.openChannelFee(OpenChannelFeeRequest(amountMsat))
} catch (e: Exception) {
    // handle error
}
React Native
try {
  const amountMsat = 10000
  const openChannelFeeResponse = await openChannelFee({
    amountMsat
  })
} catch (err) {
  console.error(err)
}
Dart
OpenChannelFeeRequest req = OpenChannelFeeRequest(amountMsat: amountMsat, expiry: expiry);
OpenChannelFeeResponse resp = await BreezSDK().openChannelFee(req: req);
print(resp.feeMsat);
Python
req = breez_sdk.OpenChannelFeeRequest(amount_msat)
channel_fees = sdk_services.open_channel_fee(req)
Go
if channelFees, err := sdk.OpenChannelFee(breez_sdk.OpenChannelFeeRequest{AmountMsat: amountMsat}); err == nil {
    log.Printf("%#v", channelFees)
}
C#
try
{
    var channelFees = sdk.OpenChannelFee(
        new OpenChannelFeeRequest(amountMsat));
}
catch (Exception)
{
    // Handle error
}
1

For more details on these fees, see Channel Opening Fees