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.
For Smart chain → Bitcoin L1 (on-chain) swaps:
- smartchain-to-btc/swapBasic.ts
- smartchain-to-btc/swapAdvancedSolana.ts
- smartchain-to-btc/swapAdvancedStarknet.ts
- smartchain-to-btc/swapAdvancedEVM.ts
For Smart chain → Lightning L2 swaps:
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_OUTmode and a BOLT11 invoice with explicit amount in sats. - Smart chain → LNURL-pay swaps allow
EXACT_INmode, support passing additional comment & reading success actions after payment.
- Smart chain → BTC
- Smart chain → Lightning
- Smart chain → LNURL-pay
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()
}
Uses the ToBTCLNSwap swap class.
import {SwapAmountType, ToBTCLNSwap} from "@atomiqlabs/sdk";
// Create a quote
const swap: ToBTCLNSwap = await swapper.swap(
Tokens.STARKNET.STRK, // Any smart chain asset, e.g. SOL on Solana, STRK on Starknet or cBTC on Citrea
Tokens.BITCOIN.BTCLN, // Swap into Lightning BTC
undefined, // Amount is determined by the passed BOLT11 lightning invoice
SwapAmountType.EXACT_OUT, // When swapping to BOLT11 lightning invoice, only EXACT_OUT mode is supported
starknetSigner.getAddress(), // Source signer address
"lnbc10u1p..." // BOLT11 lightning invoice, needs to have a fixed amount of sats!
); // Type gets infered as ToBTCLNSwap (for Lightning)
// 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}`);
}
}
);
// When swapping to lightning you can also additionally check:
// The swap will likely fail due to lightning network routing error
if (swap.willLikelyFail()) {
console.log("Lightning network payment cannot be routed, ensure the recipient is online and has channels!");
}
// Destination wallet is probably a non-custodial wallet, which needs to be online to accept the payment
if (swap.isPayingToNonCustodialWallet()) {
console.log("Destination lightning wallet is non-custodial, ensure it is online to accept the payment!");
}
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()
}
When swapping to Bitcoin Lightning and using BOLT11 lightning network invoices ("lnbc10u1p...") the invoice needs to have a fixed amount and therefore only EXACT_OUT swap mode is supported.
For swapping variable amounts in EXACT_IN mode to Bitcoin Lightning check the Smart chain → LNURL-pay tab, or the EXACT_IN Lightning Swaps section below if you have programatic API access to the destination lightning network wallet.
Uses the ToBTCLNSwap swap class.
import {SwapAmountType, ToBTCLNSwap} from "@atomiqlabs/sdk";
// Create swap with LNURL-pay or Lightning address
const swap: ToBTCLNSwap = 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
"lnurl1...", // LNURL-pay link or Lightning address ("[email protected]")
{
comment: "Payment for coffee" // Optional comment
}
); // Type gets infered as ToBTCLNSwap (for Lightning)
// When swapping to lightning you can also additionally check:
// The swap will likely fail due to lightning network routing error
if (swap.willLikelyFail()) {
console.log("Lightning network payment cannot be routed, ensure the recipient is online and has channels!");
}
// Destination wallet is probably a non-custodial wallet, which needs to be online to accept the payment
if (swap.isPayingToNonCustodialWallet()) {
console.log("Destination lightning wallet is non-custodial, ensure it is online to accept the payment!");
}
// 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!");
return;
}
// LNURL-pay link might also contain a success action, this can be displayed to the user
// upon successful payment
if (swap.hasSuccessAction()) {
const action = swap.getSuccessAction();
console.log("Description:", action.description);
console.log("Text:", action.text); // May be null
console.log("URL:", action.url); // May be null
}
For more information about how to parse LNURLs, distinguish between LNURL-withdraw and LNURL-pay variants, check out the LNURL-pay section.
Manual Execution Flow
- Signer object
- Manually sign transactions
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);
}
Type of the signer object passed to the commit() & refund() functions is dependent on the source network:
- For Solana swaps: SolanaSigner, or native
@coral-xyz/anchorWallet - For Starknet swaps: StarknetSigner, StarknetBrowserSigner, or native
starknetAccount - For EVM swaps: EVMSigner, or native
ethersSigner
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
const txsCommit = await swap.txsCommit();
... // Sign and send transactions here
// Important to wait till SDK processes the swap initialization
await swap.waitTillCommited();
// 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) {
const txsRefund = await swap.txsRefund();
... // Sign and send refund transactions here
// Important to wait till SDK processes the swap refund and updates the swap state
await swap.waitTillRefunded();
}
The transactions returned by the txs* functions use the following types:
- For Solana, uses the SolanaTx type
- For Starknet, uses the StarknetTx type
- For EVM, uses the
ethersTransactionRequest type
For more information about how to sign and send these transactions manually refer to the Manual Transactions page.
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);
}
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
| State | Value | Description |
|---|---|---|
REFUNDED | -3 | Swap failed and was refunded |
QUOTE_EXPIRED | -2 | Quote expired before execution |
QUOTE_SOFT_EXPIRED | -1 | Quote probably expired (may still succeed if init tx in flight) |
CREATED | 0 | Quote created, waiting for execution |
COMMITED | 1 | Init transaction sent |
SOFT_CLAIMED | 2 | LP processing (BTC tx sent, but not yet confirmed) |
CLAIMED | 3 | Swap complete, BTC sent |
REFUNDABLE | 4 | LP failed, can refund |
EXACT_IN Lightning Swaps
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:
- SDK sends a request to the LP saying it wants to swap
xUSDC to BTC, with a dummy invoice (either 1 sat or as specified in theminMsatsparameter - this is requested from thegetInvoice()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). - LP responds with the output amount of
yBTC - SDK calls the provided
getInvoice()callback to request the real invoice for theyamount of BTC (in satoshis) - SDK forwards the returned fixed amount (
yBTC) 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
- ToBTCSwap - Swap class for Smart Chain to BTC
- ToBTCLNSwap - Swap class for Smart Chain to Lightning
- SwapAmountType - Amount type enum
- ToBTCSwapState - Swap states