# Bitcoin → Smart Chain

Swap Bitcoin L1 (on-chain) to Starknet or EVM tokens. These swaps are based on the [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) primitive and verified through the on-chain [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md).

The user and LP cooperatively sign a single Bitcoin transaction that atomically sends BTC to the LP and authorizes the smart-chain payout to the user. Users do not need destination-chain gas upfront, and watchtowers or liquidity fronters can settle the swap automatically once the Bitcoin transaction is confirmed. The automatic settlement usually happens within a minute, if it takes longer the user can always self-claim as a fallback.

tip

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

info

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

## Executing the Swap

Here is a full flow for creating and executing the swap in the Bitcoin → Smart chain direction via the `execute()` helper function, uses the [SpvFromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap) swap class:

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



// Create a quote

const swap = await swapper.swap(

  Tokens.BITCOIN.BTC,           // Bitcoin on-chain input

  Tokens.STARKNET.STRK,         // Destination smart-chain token, e.g. STRK on Starknet or cBTC on Citrea

  "0.00003",                    // Amount (3000 sats to send)

  SwapAmountType.EXACT_IN,

  undefined,                    // Source address is not used for BTC 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 SpvFromBTCSwap



// Execute the swap

const automaticallySettled = await swap.execute(

  {

    address: "bc1q...",         // User's Bitcoin address

    publicKey: "03...",         // User's Bitcoin public key

    signPsbt: (psbt, signInputs) => {

      // Sign the PSBT with the Bitcoin wallet

      // Return the signed PSBT in hex or base64 format

      return "<signed PSBT>";

    }

  },

  {

    onSourceTransactionSent: (txId) => {

      console.log(`Bitcoin tx sent: ${txId}`);

    },

    onSourceTransactionConfirmationStatus: (txId, confirmations, target, etaMs) => {

      console.log(`Confirmations: ${confirmations}/${target}, ETA: ${etaMs / 1000}s`);

    },

    onSourceTransactionConfirmed: (txId) => {

      console.log(`Bitcoin tx confirmed: ${txId}`);

    },

    onSwapSettled: (destinationTxId) => {

      console.log(`Swap settled on destination chain: ${destinationTxId}`);

    }

  }

);



if (!automaticallySettled) {

  // Handle the edge-case when automatic settlement does not happen in time

  console.log("Automatic settlement timed out, claiming manually...");

  await swap.claim(starknetSigner);

  console.log("Claimed!");

} else {

  console.log("Success! Output transaction ID: ", swap.getOutputTxId());

}
```

warning

`execute()` requires a single-address (non-BIP32) Bitcoin wallet capable of signing PSBTs. If your signing flow is handled by an external wallet UI, or you use a BIP-32 HD wallet, use the manual **Funded PSBT** or **Raw PSBT** flow below.

## Manual Execution Flow

Choose the Bitcoin signing flow that matches your wallet integration:

* use **Bitcoin wallet** flow if you have a wallet which already implements [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) (e.g. the built-in [SingleAddressBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SingleAddressBitcoinWallet)) or [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner).
* use **Funded PSBT** if you have a single address bitcoin wallet (non-HD) and want to get a PSBT already funded with wallet input UTXOs.
* use **Raw PSBT** if you want full control of the PSBT construction, allowing you to add arbitrary inputs and outputs to the PSBT yourself.

- Bitcoin wallet
- Funded PSBT
- Raw PSBT

Pass the `bitcoinWallet` argument which follows the [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) interface or [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner) type to the [`sendBitcoinTransaction()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#sendbitcointransaction) function.

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



// Create a quote

const swap = await swapper.swap(

  Tokens.BITCOIN.BTC,

  Tokens.STARKNET.STRK,

  "0.00003",

  SwapAmountType.EXACT_IN,

  undefined,

  starknetSigner.getAddress(),

  {

    gasAmount: "0"

  }

);



// 1. Let the SDK build, sign, and submit the Bitcoin transaction through your IBitcoinWallet implementation

const bitcoinTxId = await swap.sendBitcoinTransaction(bitcoinWallet);



// 2. Wait for the Bitcoin transaction to reach the required confirmation count

await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => {

  console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`);

});



// 3. Wait for automatic settlement or fronting on the destination chain

const automaticallySettled = await swap.waitTillClaimedOrFronted(60);



// 4. If automatic settlement does not happen in time, claim manually

if (!automaticallySettled) {

  await swap.claim(starknetSigner);

}
```

Get the funded PSBT from the [`getFundedPsbt()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#getfundedpsbt) function to sign with an external single-address non-BIP32 wallet (e.g.: Xverse, Unisat, Phantom, etc.).

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



// Create a quote

const swap = await swapper.swap(

  Tokens.BITCOIN.BTC,

  Tokens.STARKNET.STRK,

  "0.00003",

  SwapAmountType.EXACT_IN,

  undefined,

  starknetSigner.getAddress(),

  {

    gasAmount: "0"

  }

);



// 1. Ask the SDK for a funded PSBT for a non-HD single-address Bitcoin wallet

const {psbtBase64, signInputs} = await swap.getFundedPsbt({

  address: "bc1q...",

  publicKey: "03..."

});



// 2. Pass psbtBase64 (or psbtHex) and signInputs to an external signer like Xverse, Unisat, Phantom, etc.

const signedPsbt = await externalBitcoinWallet.signPsbt(psbtBase64, signInputs);



// 3. Submit the signed PSBT back to the SDK - supports hexadecimal and base64 formats

const bitcoinTxId = await swap.submitPsbt(signedPsbt);



// 4. Wait for the Bitcoin transaction to reach the required confirmation count

await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => {

  console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`);

});



// 5. Wait for automatic settlement or fronting on the destination chain

const automaticallySettled = await swap.waitTillClaimedOrFronted(60);



// 6. If automatic settlement does not happen in time, claim manually

if (!automaticallySettled) {

  await swap.claim(starknetSigner);

}
```

Get the raw PSBT from the [`getPsbt`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#getpsbt) function to which you can add arbitrary inputs and outputs.

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



// Create a quote

const swap = await swapper.swap(

  Tokens.BITCOIN.BTC,

  Tokens.STARKNET.STRK,

  "0.00003",

  SwapAmountType.EXACT_IN,

  undefined,

  starknetSigner.getAddress(),

  {

    gasAmount: "0"

  }

);



// 1. Get the raw swap PSBT and fund it with your own wallet flow

const {psbt, psbtHex, psbtBase64, in1sequence} = await swap.getPsbt();

... // Add your own inputs and outputs to the PSBT



// 2. IMPORTANT: Preserve the required nSequence on the second input (input index 1)

// e.g.: psbt.updateInput(1, {sequence: in1sequence});



// 3. Sign every input except the first one, which belongs to the LP vault

...



// 4. Submit the fully funded and signed PSBT back to the SDK - supports hexadecimal and base64 formats

const bitcoinTxId = await swap.submitPsbt(psbt);



// 5. Wait for the Bitcoin transaction to reach the required confirmation count

await swap.waitForBitcoinTransaction((txId, confirmations, targetConfirmations, txEtaMs) => {

  console.log(`${confirmations}/${targetConfirmations} confirmations, ETA ${txEtaMs / 1000}s`);

});



// 6. Wait for automatic settlement or fronting on the destination chain

const automaticallySettled = await swap.waitTillClaimedOrFronted(60);



// 7. If automatic settlement does not happen in time, claim manually

if (!automaticallySettled) {

  await swap.claim(starknetSigner);

}
```

warning

Make sure that you set the second input's `nSequence` (input index `1`) to the returned `in1sequence` after funding the PSBT, then sign every input except the first one and submit the PSBT with [`submitPsbt()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap#submitpsbt).

warning

Legacy non-SegWit Bitcoin inputs are not allowed in the submitted PSBT. Use witness-type inputs such as P2WPKH, P2WSH or P2TR.

## Claiming Past Unsettled Swaps

If the app was offline and the automatic settlement did not happen, the swap can be manually claimed on the destination chain.

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.

Bitcoin → Smart chain swaps can be claimed at **any time** after the swap Bitcoin transaction gets enough confirmations. After the swap is settled on Bitcoin, the swap becomes claimable forever - there is no timelock.

## 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.BTC,

  Tokens.STARKNET.WBTC,

  "0.0001",

  SwapAmountType.EXACT_IN,

  undefined,

  starknetSigner.getAddress(),

  {

    gasAmount: "1" // Request 1 STRK as gas drop

  }

);



console.log("Gas drop:", swap.getGasDropOutput().toString());
```

warning

When already swapping to the native token of the respective destination chain (i.e. STRK on Starknet, cBTC on Citrea, etc.) don't specify the `gasAmount`, as LPs usually don't hold the necessary gas drop liquidity for those tokens! This will lead to errors during quoting and make it unable for you to request the quote.

## Swap Options

The overview of possible options you can pass to the swap via the `options` argument of `swapper.swap(...)`. For Bitcoin → Smart Chain swaps these are useful when you want to request a gas drop, cap the accepted Bitcoin fee rate, or tune automatic-settlement incentives.

| Name                       | Type               | Description                                                                                                                                                                                                                          |
| -------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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. |
| `maxAllowedBitcoinFeeRate` | `number`           | Upper bound, in sats/vB, on the LP-required minimum Bitcoin fee rate for the funding transaction.<br />Default: computed dynamically as `10 + currentBitcoinFeeRate * 1.5`.                                                          |
| `unsafeZeroWatchtowerFee`  | `boolean`          | Attaches zero watchtower fee to the swap. This can make automatic settlement unattractive, so you may need to settle manually with `swap.claim(...)`.<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 [SpvFromBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SpvFromBTCSwapState) 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 {SpvFromBTCSwapState, SwapStateInfo} from "@atomiqlabs/sdk";



const state: SpvFromBTCSwapState = swap.getState();

console.log(`State (numeric): ${state}`);



const richState: SwapStateInfo<SpvFromBTCSwapState> = 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: SpvFromBTCSwapState = swap.getState();

});
```

### Table of States

| State                | Value | Description                                                                                            |
| -------------------- | ----- | ------------------------------------------------------------------------------------------------------ |
| `CLOSED`             | -5    | Catastrophic failure has occurred when processing the swap on the smart-chain side.                    |
| `FAILED`             | -4    | Some of the bitcoin swap transaction inputs were double-spent, so the swap failed and no BTC was sent. |
| `DECLINED`           | -3    | The intermediary (LP) declined to co-sign the submitted PSBT.                                          |
| `QUOTE_EXPIRED`      | -2    | Swap has expired for good and there is no way it can be executed anymore.                              |
| `QUOTE_SOFT_EXPIRED` | -1    | Swap is almost expired and should be presented as expired, though it still might be processed.         |
| `CREATED`            | 0     | Swap was created, waiting for the user to sign and submit the Bitcoin swap PSBT.                       |
| `SIGNED`             | 1     | Swap Bitcoin PSBT was submitted by the client to the SDK.                                              |
| `POSTED`             | 2     | Swap PSBT was sent to the intermediary (LP), waiting for co-sign and broadcast.                        |
| `BROADCASTED`        | 3     | The intermediary co-signed and broadcasted the Bitcoin transaction.                                    |
| `FRONTED`            | 4     | Settlement on the destination smart chain was fronted and funds were received before final settlement. |
| `BTC_TX_CONFIRMED`   | 5     | Bitcoin transaction has the required confirmations, waiting for automatic settlement or manual claim.  |
| `CLAIMED`            | 6     | Swap settled on the smart chain and funds were received.                                               |

## API Reference

* [SpvFromBTCSwap](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/SpvFromBTCSwap) - Swap class for Bitcoin → Smart chain
* [SwapAmountType](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SwapAmountType) - Amount type enum
* [SpvFromBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/SpvFromBTCSwapState) - Swap states
