Lightning → Smart Chain
Swap Bitcoin Lightning to Starknet or EVM tokens. These swaps are based on an LP-initiated HTLC on the destination chain. After the LP receives the Lightning payment, it creates the HTLC and the user broadcasts the payment secret over Nostr so watchtowers can claim on the user's behalf. The user can always self-claim as a fallback.
Solana uses a different (legacy) swap protocol. See Lightning to Solana.
Executing the Swap
Here is a full flow for creating and executing the swap in the Lightning → Smart chain direction via the execute() helper function. The flow is the same for all the cases, with the only difference being how the Lightning payment is sourced.
Uses the FromBTCLNAutoSwap swap class.
- Lightning wallet
- External payment
- LNURL-withdraw
Pass a MinimalLightningNetworkWalletInterface object to pay the invoice programmatically.
import {MinimalLightningNetworkWalletInterface, SwapAmountType} from "@atomiqlabs/sdk";
// Create a quote
const swap = await swapper.swap(
Tokens.BITCOIN.BTCLN, // Lightning BTC input
Tokens.STARKNET.STRK, // Destination smart-chain token, e.g. STRK on Starknet or cBTC on Citrea
10_000n, // Amount in sats
SwapAmountType.EXACT_IN,
undefined, // Source address is not used for standard Lightning source swaps
starknetSigner.getAddress(), // Destination smart-chain address
{
gasAmount: 0n // Optional: request native destination token as gas drop
}
); // Type gets inferred as FromBTCLNAutoSwap
// Execute the swap
const automaticallySettled = await swap.execute(
{
payInvoice: async (bolt11) => {
// Pay the BOLT11 invoice with WebLN, NWC, or any other Lightning wallet integration
return ""; // Empty string is fine if the wallet does not expose the payment preimage
}
}, // Pass the MinimalLightningNetworkWalletInterface object
{
onSourceTransactionReceived: (paymentHash) => {
console.log(`Lightning payment received by LP: ${paymentHash}`);
},
onSwapSettled: (destinationTxId) => {
console.log(`Swap settled on destination chain: ${destinationTxId}`);
}
}
);
if (!automaticallySettled) {
// Handle the edge-case when watchtowers do not settle the HTLC automatically
console.log("Automatic settlement timed out, claiming manually...");
await swap.claim(starknetSigner);
console.log("Claimed!");
} else {
console.log("Success! Output transaction ID:", swap.getOutputTxId());
}
Pay the invoice or lightning: deeplink, externally then wait for the payment to be received by passing null or undefined to the execute() function.
Make sure to call execute() on the swap before you display the lightning invoice to the user. This ensures the swap is properly initiated before receiving the lightning network payment.
import {SwapAmountType} from "@atomiqlabs/sdk";
// Create a quote
const swap = await swapper.swap(
Tokens.BITCOIN.BTCLN,
Tokens.STARKNET.STRK,
10_000n,
SwapAmountType.EXACT_IN,
undefined,
starknetSigner.getAddress(),
{
gasAmount: 0n // Optional: request native destination token as gas drop
}
);
// Show the Lightning invoice or `lightning:` deeplink to the user
const lightningInvoice = swap.getAddress();
const lightningUri = swap.getHyperlink();
// Wait for an external payment to the invoice
const automaticallySettled = await swap.execute(
undefined, // You can also pass `null` here
{
onSourceTransactionReceived: (paymentHash) => {
console.log(`Lightning payment received by LP: ${paymentHash}`);
},
onSwapSettled: (destinationTxId) => {
console.log(`Swap settled on destination chain: ${destinationTxId}`);
}
}
);
if (!automaticallySettled) {
// Handle the edge-case when watchtowers do not settle the HTLC automatically
console.log("Automatic settlement timed out, claiming manually...");
await swap.claim(starknetSigner);
console.log("Claimed!");
} else {
console.log("Success! Output transaction ID:", swap.getOutputTxId());
}
Pass an LNURL-withdraw link string or LNURLWithdraw object to source the payment from LNURL-withdraw.
import {LNURLWithdraw, SwapAmountType} from "@atomiqlabs/sdk";
// Create a quote
const swap = await swapper.swap(
Tokens.BITCOIN.BTCLN,
Tokens.STARKNET.STRK,
10_000n,
SwapAmountType.EXACT_IN,
undefined,
starknetSigner.getAddress(),
{
gasAmount: 0n // Optional: request native destination token as gas drop
}
);
const automaticallySettled = await swap.execute(
"lnurl1...", // Pass an LNURL-withdraw link or parsed LNURLWithdraw object to source the payment
{
onSourceTransactionReceived: (paymentHash) => {
console.log(`Lightning payment received by LP: ${paymentHash}`);
},
onSwapSettled: (destinationTxId) => {
console.log(`Swap settled on destination chain: ${destinationTxId}`);
}
}
);
if (!automaticallySettled) {
// Handle the edge-case when watchtowers do not settle the HTLC automatically
console.log("Automatic settlement timed out, claiming manually...");
await swap.claim(starknetSigner);
console.log("Claimed!");
} else {
console.log("Success! Output transaction ID:", swap.getOutputTxId());
}
For more information about how to parse LNURLs, distinguish between LNURL-withdraw and LNURL-pay variants, check out the LNURL-withdraw section.
Ensure that the SDK stays online when the lightning network payment is sent, this is necessary since the SDK needs to actively broadcast the swap secret over Nostr upon receiving the swap HTLC.
Manual Execution Flow
import {SwapAmountType} from "@atomiqlabs/sdk";
// Create a quote
const swap = await swapper.swap(
Tokens.BITCOIN.BTCLN,
Tokens.STARKNET.STRK,
10_000n,
SwapAmountType.EXACT_IN,
undefined,
starknetSigner.getAddress()
);
// 1. Show the invoice to the user
const lightningInvoice = swap.getAddress();
const lightningUri = swap.getHyperlink();
// 2. Wait for the Lightning payment and for the LP to create the destination HTLC
// This is important to make sure the swap is properly initiated before the user attempts a lightning network payment
const paymentReceived = await swap.waitForPayment();
if (!paymentReceived) {
console.log("Lightning payment was not received before the quote expired.");
return;
}
// 3. Wait for watchtower settlement on the destination chain
const automaticallySettled = await swap.waitTillClaimed(60);
// 4. If watchtowers do not settle in time, claim manually
if (!automaticallySettled) {
await swap.claim(starknetSigner);
}
If you need to sign the destination-chain claim manually, use txsClaim() instead of claim().
The returned transactions use the following types:
- 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.
Claiming Past Unsettled Swaps
If the app was offline when the LP created the destination HTLC and/or the watchtowers didn't settle the swap automatically, the swap can still be manually claimed as long as the HTLC has not expired yet.
Checking if a single swap is claimable and claiming it:
if (swap.isClaimable()) {
await swap.claim(starknetSigner);
}
Getting all swaps that are claimable and claiming them:
const claimable = await swapper.getClaimableSwaps(
"STARKNET", // Only get claimable swaps on STARKNET
starknetSigner.getAddress() // Only get claimable swaps for this destination address
); // This returns all claimable swaps, which could be either Bitcoin → Smart chain or Lightning → Smart chain
for (const swap of claimable) {
// All the claimable swap types have the same `claim()` function signature
await swap.claim(starknetSigner);
}
It is a good practice to query claimable swaps on your app's startup and either claim them automatically or prompt the user to claim them.
Unlike Bitcoin → Smart chain swaps, Lightning → Smart chain swaps are only claimable until the destination HTLC expires, so this recovery check should run promptly.
Gas Drop
You can request native tokens on the destination chain along with the main swap output. This helps cover gas fees for the first transactions on the destination chain.
const swap = await swapper.swap(
Tokens.BITCOIN.BTCLN,
Tokens.STARKNET.WBTC,
20_000n,
SwapAmountType.EXACT_IN,
undefined,
starknetSigner.getAddress(),
{
gasAmount: 1_000_000_000_000_000_000n // Request 1 STRK as gas drop
}
);
console.log("Main output:", swap.getOutput().toString());
console.log("Gas drop:", swap.getGasDropOutput().toString());
LNURL-withdraw
LNURL-withdraw links (lnurl1...) can be used as the source of a Lightning → Smart chain swap, letting the SDK generate a BOLT11 invoice for the quote and submit it to the LNURL-withdraw service automatically when you call waitForPayment() or execute().
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.
import {isLNURLWithdraw, LNURLPay, LNURLWithdraw, SwapAmountType} from "@atomiqlabs/sdk";
// LNURL-withdraw link
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(lnurlOrLightningAddress);
// Check if the LNURL is of the LNURL-withdraw type
if (!isLNURLWithdraw(lnurlDetails))
throw new Error("LNURL isn't of the LNURL-withdraw type!");
// You can inspect the LNURL-withdraw details before quoting
const swapMinimumSats: bigint = lnurlDetails.min;
const swapMaximumSats: bigint = lnurlDetails.max;
const defaultDescription: string = lnurlDetails.params.defaultDescription;
// Create swap with LNURL-withdraw as the Lightning source
const swap = await swapper.swap(
Tokens.BITCOIN.BTCLN,
Tokens.STARKNET.STRK,
10_000n,
SwapAmountType.EXACT_IN,
lnurlDetails, // Fetched details or raw LNURL-withdraw link string
starknetSigner.getAddress()
);
// No wallet is needed, funds come from the LNURL-withdraw source
const automaticallySettled = await swap.execute(
undefined,
{
onSourceTransactionReceived: (paymentHash) => {
console.log(`Lightning payment received by LP: ${paymentHash}`);
},
onSwapSettled: (destinationTxId) => {
console.log(`Swap settled on destination chain: ${destinationTxId}`);
}
}
);
if (!automaticallySettled) {
await swap.claim(starknetSigner);
}
If you created a normal Lightning quote first and later want to fund it from an LNURL-withdraw source instead, call settleWithLNURLWithdraw(lnurl) before or during waitForPayment()/execute(). This is useful when you want to display a standard invoice QR code and also allow LNURL-withdraw or NFC-card settlement.
Swap States
Read the current state of the swap in its FromBTCLNAutoSwapState enum form with getState() or in human readable SwapStateInfo form with description with getStateInfo():
import {FromBTCLNAutoSwapState, SwapStateInfo} from "@atomiqlabs/sdk";
const state: FromBTCLNAutoSwapState = swap.getState();
console.log(`State (numeric): ${state}`);
const richState: SwapStateInfo<FromBTCLNAutoSwapState> = 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: FromBTCLNAutoSwapState = swap.getState();
});
Table of States
| State | Value | Description |
|---|---|---|
FAILED | -4 | The destination HTLC was not claimed before expiry, so the Lightning payment refunds. |
QUOTE_EXPIRED | -3 | Swap quote expired and can no longer be executed. |
QUOTE_SOFT_EXPIRED | -2 | Swap should be treated as expired, though it might still succeed if payment is already in flight. |
EXPIRED | -1 | The destination HTLC expired, so it is no longer safe to claim and the incoming Lightning payment will refund. |
PR_CREATED | 0 | Quote created. Pay the invoice from getAddress() or getHyperlink(), then wait with waitForPayment(). |
PR_PAID | 1 | The LP received the Lightning payment, but the destination HTLC is not created yet. Continue waiting with waitForPayment(). |
CLAIM_COMMITED | 2 | The destination HTLC exists. Wait for watchtowers with waitTillClaimed() or claim manually with claim() / txsClaim(). |
CLAIM_CLAIMED | 3 | Swap settled on the destination chain and the Lightning payment can complete. |
API Reference
- FromBTCLNAutoSwap - Swap class for Lightning → Smart chain
- SwapAmountType - Amount type enum
- FromBTCLNAutoSwapState - Swap states