# Lightning → Smart Chain

Swap Bitcoin Lightning to Starknet or EVM tokens. These swaps are based on an LP-initiated [HTLC](https://docs.atomiq.exchange/overview/core-primitives/htlc.md) 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](https://docs.atomiq.exchange/overview/actors.md) can claim on the user's behalf. The automatic settlement via watchtowers should complete within a minute, if it takes longer the user can always self-claim as a fallback.

tip

* [btcln-to-smartchain/swapBasic.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btcln-to-smartchain/swapBasic.ts)
* [btcln-to-smartchain/swapBasicLNURL.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btcln-to-smartchain/swapBasicLNURL.ts)
* [btcln-to-smartchain/swapAdvancedStarknet.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btcln-to-smartchain/swapAdvancedStarknet.ts)
* [btcln-to-smartchain/swapAdvancedEVM.ts](https://github.com/atomiqlabs/atomiq-sdk-demo/blob/main/src/btcln-to-smartchain/swapAdvancedEVM.ts)

info

Solana uses a different (legacy) swap protocol. See [Lightning to Solana](https://docs.atomiq.exchange/sdk-guide/swaps/solana/lightning-to-solana.md).

## 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](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap) swap class.

* Lightning wallet
* External payment
* LNURL-withdraw

Pass a [MinimalLightningNetworkWalletInterface](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/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: "0"               // Optional: request native destination token as gas drop

    // For additional options checks the `Swap Options` section

  }

); // 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.

warning

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: "0"               // Optional: request native destination token as gas drop

    // For additional options checks the `Swap Options` section

  }

);



// 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](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/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: "0"               // Optional: request native destination token as gas drop

    // For additional options checks the `Swap Options` section

  }

);



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());

}
```

info

For more information about how to parse LNURLs, distinguish between LNURL-withdraw and LNURL-pay variants, check out the [LNURL-withdraw](#lnurl-withdraw) section.

warning

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);

}
```

info

If you need to sign the destination-chain claim manually, use [`txsClaim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#txsclaim) instead of [`claim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#claim).

The returned transactions use the following types:

* For Starknet, uses the [StarknetTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-starknet/src/type-aliases/StarknetTx) type
* For EVM, uses the `ethers` [TransactionRequest](https://docs.ethers.org/v6/api/providers/#TransactionRequest) type

For more information about how to sign and send these transactions manually refer to the [Manual Transactions](https://docs.atomiq.exchange/sdk-guide/advanced/manual-transactions.md) 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);

}
```

info

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" // 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()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#waitforpayment) or [`execute()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap#execute).

You can parse LNURLs either with the generic [`SwapperUtils.parseAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#parseaddress) function exposed by the swapper as `swapper.Utils.parseAddress()` or with the specific [`SwapperUtils.getLNURLTypeAndData()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SwapperUtils#getlnurltypeanddata) function, which return the [LNURLPay](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/LNURLPay) or [LNURLWithdraw](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/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);

}
```

tip

If you created a normal Lightning quote first and later want to fund it from an LNURL-withdraw source instead, call [`settleWithLNURLWithdraw(lnurl)`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNSwap#settlewithlnurlwithdraw) 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 Options

The overview of possible options you can pass to the swap via the `options` argument of `swapper.swap(...)`. For Lightning → Smart Chain swaps these are useful when you want to request a gas drop, attach Lightning invoice metadata, or adjust automatic-settlement and safety-check behavior.

| Name                      | Type               | Description                                                                                                                                                                                                                                                    |
| ------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentHash`             | `Buffer \| string` | Lets you supply your own payment hash instead of letting the SDK generate it. When set, the swap will not settle automatically until you later provide the preimage/secret manually during claim or waiting.<br />Default: generated automatically by the SDK. |
| `description`             | `string`           | Optional Lightning invoice description. Keep it below 500 UTF-8 bytes.<br />Default: none.                                                                                                                                                                     |
| `descriptionHash`         | `Buffer \| string` | Optional Lightning invoice description hash, useful when the invoice is exposed through LNURL-pay.<br />Default: none.                                                                                                                                         |
| `gasAmount`               | `bigint \| string` | Optional extra native-token gas drop to receive on the destination chain. `bigint` is in base units, `string` is a human-readable decimal amount. Do not use it when swapping to the native token itself.<br />Default: no gas drop.                           |
| `unsafeSkipLnNodeCheck`   | `boolean`          | Skips checking whether the LP's Lightning node has enough channel liquidity for the swap.<br />Default: `false`.                                                                                                                                               |
| `unsafeZeroWatchtowerFee` | `boolean`          | Attaches zero watchtower fee to the swap. This can prevent automatic settlement and leave you with a manual `swap.claim(...)` flow.<br />Default: `false`.                                                                                                     |
| `feeSafetyFactor`         | `number`           | Multiplier used when estimating the watchtower fee. Higher values make automatic settlement more attractive.<br />Default: `1.25`.                                                                                                                             |

## Swap States

Read the current state of the swap in its [FromBTCLNAutoSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCLNAutoSwapState) enum form with [`getState()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getstate) or in human readable [SwapStateInfo](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/SwapStateInfo) form with description with [`getStateInfo()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#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](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCLNAutoSwap) - Swap class for Lightning → Smart chain
* [SwapAmountType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapAmountType) - Amount type enum
* [FromBTCLNAutoSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCLNAutoSwapState) - Swap states
