Skip to main content

Smart Chain → BTC/Lightning

Swap smart chain tokens (Solana, Starknet & EVM tokens) to Bitcoin L1 (on-chain) or Lightning L2. These swaps are based on the HTLC (for lightning) & PrTLC (for on-chain) primitives.

Executing the Swap

Here is a full flow for creating an executing the swap in the Smart Chain → BTC/Lightning direction via the execute() helper function. The flow is the same for all the cases, with the only differences being:

  • Smart chain → Lightning swaps require EXACT_OUT mode and a BOLT11 invoice with explicit amount in sats.
  • Smart chain → LNURL-pay swaps allow EXACT_IN mode, support passing additional comment & reading success actions after payment.

Uses the ToBTCSwap swap class.

import {SwapAmountType, ToBTCSwap} from "@atomiqlabs/sdk";

// Create a quote
const swap: ToBTCSwap = await swapper.swap(
Tokens.STARKNET.STRK, // Any smart chain asset, e.g. SOL on Solana, STRK on Starknet or cBTC on Citrea
Tokens.BITCOIN.BTC, // Or `Tokens.BITCOIN.BTCLN` when swapping into Lightning
"0.00003", // Amount (3000 sats to receive)
SwapAmountType.EXACT_OUT, // NOTE: When swapping to BOLT11 lightning invoice, only EXACT_OUT mode is supported
starknetSigner.getAddress(), // Source signer address
"bc1q..." // Bitcoin on-chain address, BOLT11 lightning invoice or LNURL-pay link
); // Type gets infered as ToBTCSwap (for Bitcoin on-chain)

// Execute the swap
const swapSuccessful = await swap.execute(
starknetSigner, // Pass in the signer on the source chain to sign the transactions
{
onSourceTransactionSent: (txId) => {
console.log(`Source tx sent: ${txId}`);
},
onSourceTransactionConfirmed: (txId) => {
console.log(`Source tx confirmed: ${txId}`);
},
onSwapSettled: (btcTxId) => {
console.log(`Bitcoin tx sent: ${btcTxId}`);
}
}
);

if (!swapSuccessful) {
// Handle the edge-case when LP fails to process the swap
console.log("Swap failed, refunding...");
// Refund funds back to the
await swap.refund(starknetSigner);
console.log("Refunded!");
} else {
console.log("Success! Output transaction ID: ", swap.getOutputTxId());
// For lightning network swaps you can get the payment preimage with swap.getSecret()
}

Manual Execution Flow

import {SwapAmountType} from "@atomiqlabs/sdk";

// Create a quote
const swap = await swapper.swap(
Tokens.STARKNET.STRK, // Any smart chain asset, e.g. SOL on Solana, STRK on Starknet or cBTC on Citrea
Tokens.BITCOIN.BTC, // Or `Tokens.BITCOIN.BTCLN` when swapping into Lightning
"0.00003", // Amount (3000 sats to receive)
SwapAmountType.EXACT_OUT, // NOTE: When swapping to BOLT11 lightning invoice, only EXACT_OUT mode is supported
starknetSigner.getAddress(), // Source signer address
"bc1q..." // Bitcoin on-chain address, BOLT11 lightning invoice or LNURL-pay link
); // Type gets infered as ToBTCSwap (for Bitcoin on-chain) or ToBTCLNSwap (for Lightning)

// 1. Initiate the swap on the source chain
await swap.commit(starknetSigner);

// 2. Wait for the swap to execute and for the destination tx to be sent
const swapSuccessful = await swap.waitForPayment();

// 3. Refund in case of failure
if(!swapSuccessful) {
await swap.refund(starknetSigner);
}
info

Type of the signer object passed to the commit() & refund() functions is dependent on the source network:

Refunding Past Failed Swaps

The swaps might only transition into the refundable state after several hours (in case of LP non-cooperativeness combined with Lightning swaps even multiple days). Therefore there are a few helper functions to allow you to determine whether the swap is already refundable & to list all refundable swaps.

Checking if a single swap is refundable and refunding it:

if (swap.isRefundable()) {
await swap.refund(signer);
}

Getting all swaps that are refundable and refunding them:

const refundable = await swapper.getRefundableSwaps(
"STARKNET", // Allows you to specify to only get refundable swaps on `STARKNET`
starknetSigner.getAddress() // Allows you to specify to only get refundable swpas for the signer address
);

for (const swap of refundable) {
await swap.refund(starknetSigner);
}
info

It is a good practice to get the refundable swaps on your app's startup and either refunding them automatically or prompting the user to refund.

LNURL-pay

LNURL-pay links ("lnurl1p...") and static lightning addresses ("[email protected]") are reusable and allow payer to set an amount. Hence, they can be used to swap into Lightning with EXACT_IN mode.

Additionally, they also support attaching an optional comment to the payment (which you can pass in the swap() function options), and also expose a success action, which can be displayed to the user after a successful payment.

You can parse LNURLs either with the generic SwapperUtils.parseAddress() function exposed by the swapper as swapper.Utils.parseAddress() or with the specific SwapperUtils.getLNURLTypeAndData() function, which return the LNURLPay or LNURLWithdraw object allowing you to read the LNURL's details.

// LNURL-pay link or Lightning address ("[email protected]")
const lnurlOrLightningAddress: string = "lnurl1...";
// Check if it has correct format
if(!swapper.Utils.isValidLNURL(lnurlOrLightningAddress))
throw new Error("Invalid LNURL specified!");
// Fetch the details of the LNURL, it can be either LNURL-pay or LNURL-withdraw
const lnurlDetails: LNURLPay | LNURLWithdraw = await swapper.Utils.getLNURLTypeAndData();
// Check if the LNURL is of the LNURL-pay type
if(!isLNURLPay(lnurlDetails))
throw new Error("LNURL isn't of the LNURL-pay type!");

//You can check the details of the LNURL-pay link, for the full details refer to the `LNURLPay` typedoc page.
const swapMinimumSats: bigint = lnurlDetails.min;
const swapMaximumSats: bigint = lnurlDetails.max;
const shortDescription: string = lnurlDetails.shortDescription;

// Create swap with LNURL-pay or Lightning address
const swap = await swapper.swap(
Tokens.STARKNET.STRK, // Swap STRK
Tokens.BITCOIN.BTCLN, // To Lightning BTC
"100", // Now we can specify amount, and even an input amount!
SwapAmountType.EXACT_IN, // With LNURL-pay we can use EXACT_IN mode on Lightning!
starknetSigner.getAddress(), // Source signer address
lnurlDetails, // Fetched details or raw LNURL-pay link or Lightning address string
{
comment: "Payment for coffee" // Optional comment
}
);

... // Handle swap execution

Swap States

Read the current state of the swap with either in its ToBTCSwapState enum form with getState() or in human readable SwapStateInfo form with description with getStateInfo():

import {ToBTCSwapState, SwapStateInfo} from "@atomiqlabs/sdk";

const state: ToBTCSwapState = swap.getState();
console.log(`State state (numeric): ${state}`);

const richState: SwapStateInfo<ToBTCSwapState> = swap.getStateInfo();
console.log(`State name: ${richState.name}`);
console.log(`State description: ${richState.description}`);

Subscribe to swap state updates with:

swap.events.on("swapState", () => {
const state: ToBTCSwapState = swap.getState();
});

Table of States

StateValueDescription
REFUNDED-3Swap failed and was refunded
QUOTE_EXPIRED-2Quote expired before execution
QUOTE_SOFT_EXPIRED-1Quote probably expired (may still succeed if init tx in flight)
CREATED0Quote created, waiting for execution
COMMITED1Init transaction sent
SOFT_CLAIMED2LP processing (BTC tx sent, but not yet confirmed)
CLAIMED3Swap complete, BTC sent
REFUNDABLE4LP failed, can refund

EXACT_IN Lightning Swaps

info

This is an advanced feature, you should only use this if you have programmatic API to fetch invoices from a lightning network wallet and want to support EXACT_IN swaps.

The main limitation of regular lightning network swaps (Smart chains → Lightning), is the fact that exactIn swaps are not possible (as invoices need to have a fixed amount). LNURL-pay links solve this issue, but are not supported by all the wallets. Therefore, the SDK exposes a hook/callback that can be implemented by lightning wallets directly, which request fixed amount invoices on-demand. This then makes exact input amount swaps possible. The way it works:

  1. SDK sends a request to the LP saying it wants to swap x USDC to BTC, with a dummy invoice (either 1 sat or as specified in the minMsats parameter - this is requested from the getInvoice() function) - this dummy invoice is used to estimate the routing fees by the LP (extra care must be taken for both invoices, dummy and the real one to have the same destination node public key & routing hints).
  2. LP responds with the output amount of y BTC
  3. SDK calls the provided getInvoice() callback to request the real invoice for the y amount of BTC (in satoshis)
  4. SDK forwards the returned fixed amount (y BTC) lightning network invoice back to the LP to finish creating the quote

To get the EXACT_IN quote for Smart Chain → Lightning swap from the SDK:

const swap = await swapper.swap(
Tokens.STARKNET.STRK,
Tokens.BITCOIN.BTCLN,
"100", // 100 STRK input
SwapAmountType.EXACT_IN,
starknetSigner.getAddress(),
{
getInvoice: async (amountSats, abortSignal?) => {
// Generate invoice for the calculated output amount
const invoice = await myLnWallet.createInvoice(amountSats);
return invoice;
},
minMsats: 1_000_000n, // Optional: 1000 sats minimum
maxMsats: 1_000_000_000n // Optional: 1M sats maximum
}
);

API Reference