# Bitcoin → Solana

Swap Bitcoin L1 (on-chain) to Solana tokens. Solana still uses the legacy [PrTLC](https://docs.atomiq.exchange/overview/core-primitives/prtlc.md) based Bitcoin → smart chain flow, verified through the on-chain [Bitcoin light client](https://docs.atomiq.exchange/overview/core-primitives/bitcoin-light-client.md). Unlike the newer [UTXO-controlled vault](https://docs.atomiq.exchange/overview/core-primitives/utxo-controlled-vault.md) protocol used on Starknet and EVM, the user must first initialize a destination-side swap escrow on Solana and lock a SOL deposit before sending BTC.

The user first pre-locks LP funds in the Solana-side PrTLC, sends the exact BTC amount to a dedicated swap address, and then a watchtower or the user proves the confirmed Bitcoin payment on Solana to claim the output. Because the user must post a security deposit and watchtower bounty in SOL on swap initation, this legacy flow has a cold-start requirement that the newer protocol avoids.

info

Starknet and EVM use the newer UTXO-controlled vault protocol. See [Bitcoin → Smart Chain](https://docs.atomiq.exchange/sdk-guide/swaps/btc-to-smart-chain.md).

## Executing the Swap

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

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



// Create a quote

const swap = await swapper.swap(

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

  Tokens.SOLANA.SOL,          // Destination Solana token

  "0.0001",                   // Amount (10,000 sats to send)

  SwapAmountType.EXACT_IN,

  undefined,                  // Source address is not used for BTC source swaps

  solanaSigner.getAddress(),  // Destination Solana address

  {

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

  }

); // Type gets inferred as FromBTCSwap



// Inspect the native-SOL amounts locked during initialization

console.log("Security deposit:", swap.getSecurityDeposit().toString());

console.log("Claimer bounty:", swap.getClaimerBounty().toString());



// Execute the swap

const automaticallySettled = await swap.execute(

  solanaSigner,               // Destination signer used to open the PrTLC escrow on Solana

  {

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

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

    signPsbt: (psbt, signInputs) => {

      // Sign the funded PSBT with the Bitcoin wallet

      // Return the signed PSBT in hex or base64 format

      return "<signed PSBT>";

    }

  },

  {

    onDestinationCommitSent: (txId) => {

      console.log(`Swap escrow opened on Solana: ${txId}`);

    },

    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 Solana: ${destinationTxId}`);

    }

  }

);



if (!automaticallySettled) {

  // Handle the edge-case when watchtowers do not settle the PrTLC in time

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

  await swap.claim(solanaSigner);

  console.log("Claimed!");

} else {

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

}
```

warning

`execute()` requires a Solana signer because the legacy Solana flow must open the destination-side PrTLC before a usable Bitcoin swap address exists, use [SolanaSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/classes/SolanaSigner), or native `@coral-xyz/anchor` [Wallet](https://solana-foundation.github.io/anchor/ts/classes/Wallet.html).

This flow assumes a single-address (non-HD) Bitcoin wallet capable of signing PSBTs. If you want to handle the Bitcoin payment manually from an external wallet instead, use the manual flow below.

## Manual Execution Flow

Choose the Bitcoin payment flow that matches your wallet integration:

* use **Bitcoin wallet** if you already implement [IBitcoinWallet](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/interfaces/IBitcoinWallet) or [MinimalBitcoinWalletInterfaceWithSigner](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/type-aliases/MinimalBitcoinWalletInterfaceWithSigner) and want the SDK to send the BTC transaction after the Solana commit step.
* use **Funded PSBT** if you want a signable PSBT for a single-address non-HD Bitcoin wallet.
* use **External wallet** if you want to pay the exact amount from an external wallet via [`getAddress()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#getaddress) or [`getHyperlink()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#gethyperlink).

- Bitcoin wallet
- Funded PSBT
- External wallet

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/FromBTCSwap#sendbitcointransaction) function.

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



// Create a quote

const swap = await swapper.swap(

  Tokens.BITCOIN.BTC,

  Tokens.SOLANA.SOL,

  "0.0001",

  SwapAmountType.EXACT_IN,

  undefined,

  solanaSigner.getAddress()

);



// 1. Commit the swap on Solana to open the Bitcoin swap address

await swap.commit(solanaSigner);



// 2. Let the SDK build, sign, and send the Bitcoin transaction through your wallet integration

const bitcoinTxId = await swap.sendBitcoinTransaction(bitcoinWallet);



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

});



// 4. Wait for watchtower settlement on Solana

const automaticallySettled = await swap.waitTillClaimed(30);



// 5. If watchtowers do not settle in time, claim manually

if (!automaticallySettled) {

  await swap.claim(solanaSigner);

}
```

Get the funded PSBT from the [`getFundedPsbt()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#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.SOLANA.SOL,

  "0.0001",

  SwapAmountType.EXACT_IN,

  undefined,

  solanaSigner.getAddress()

);



// 1. Commit the swap on Solana to open the Bitcoin swap address

await swap.commit(solanaSigner);



// 2. 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..."

});



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

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



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

const bitcoinTxId = await swap.submitPsbt(signedPsbt);



// 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 watchtower settlement on Solana

const automaticallySettled = await swap.waitTillClaimed(30);



// 7. If watchtowers do not settle in time, claim manually

if (!automaticallySettled) {

  await swap.claim(solanaSigner);

}
```

Use the committed Bitcoin swap address directly from an external wallet. This is the legacy Solana-specific flow that does not require PSBT signing, but the paid amount must match [`getInput()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getinput) exactly.

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



// Create a quote

const swap = await swapper.swap(

  Tokens.BITCOIN.BTC,

  Tokens.SOLANA.SOL,

  "0.0001",

  SwapAmountType.EXACT_IN,

  undefined,

  solanaSigner.getAddress()

);



// 1. Commit the swap on Solana to open the Bitcoin swap address

await swap.commit(solanaSigner);



// 2. Show the exact-address payment request to the external Bitcoin wallet

const btcSwapAddress = swap.getAddress();

const btcDeepLink = swap.getHyperlink();

const exactInputAmount = swap.getInput().toString();



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

});



// 4. Wait for watchtower settlement on Solana

const automaticallySettled = await swap.waitTillClaimed(30);



// 5. If watchtowers do not settle in time, claim manually

if (!automaticallySettled) {

  await swap.claim(solanaSigner);

}
```

warning

`getAddress()` and `getHyperlink()` only work after the swap has been committed. When paying from an external wallet, the amount must match [`getInput()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/ISwap#getinput) exactly. Sending a different amount can lead to loss of funds.

info

If you need to sign the Solana transactions manually, use [`txsCommit()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#txscommit) instead of [`commit()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#commit) and [`txsClaim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#txsclaim) instead of [`claim()`](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/classes/FromBTCSwap#claim). The returned transactions use the [SolanaTx](https://docs.atomiq.exchange/sdk-reference/api/atomiq-chain-solana/src/type-aliases/SolanaTx) 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 and the watchtower did not settle the PrTLC automatically, the swap can still be manually claimed on Solana once the Bitcoin transaction reaches the required confirmations.

Checking if a single swap is claimable and claiming it:

```
if (swap.isClaimable()) {

  await swap.claim(solanaSigner);

}
```

Getting all swaps that are claimable and claiming them:

```
const claimable = await swapper.getClaimableSwaps(

  "SOLANA",                  // Only get claimable swaps on SOLANA

  solanaSigner.getAddress()  // Only get claimable swaps for this destination address

); // This returns claimable Bitcoin → Solana and Lightning → Solana swaps



for (const swap of claimable) {

  // All the claimable swap types have the same `claim()` function signature

  await swap.claim(solanaSigner);

}
```

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.

This legacy flow is more time-sensitive than the newer Bitcoin → Smart chain protocol, because the LP can refund the PrTLC after expiry if no successful claim happens in time.

## Native SOL Requirements

Legacy Bitcoin → Solana swaps require native SOL for both the pre-funded destination-side security deposit, claimer bounty and the Solana transaction fees needed to open the PrTLC.

* `getSecurityDeposit()`: the slashable SOL deposit that the LP can keep if the user never sends the Bitcoin payment after initiating the swap on Solana.
* `getClaimerBounty()`: the SOL reward reserved for watchtowers that claim the swap automatically after Bitcoin confirmation.
* `getTotalDeposit()`: the total SOL amount locked during initialization, combining the security deposit and claimer bounty (a maximum of claimer bounty and security deposit, not a sum).

You can inspect the total native-token requirements before calling `commit()` or `execute()`:

```
const feeCheck = await swap.hasEnoughForTxFees();



console.log("Enough SOL:", feeCheck.enoughBalance);

console.log("Wallet balance:", feeCheck.balance.toString());

console.log("Required SOL:", feeCheck.required.toString());



console.log("Security deposit:", swap.getSecurityDeposit().toString());

console.log("Claimer bounty:", swap.getClaimerBounty().toString());

console.log("Total locked deposit:", swap.getTotalDeposit().toString());
```

warning

You need enough SOL to cover the legacy deposit requirement and the destination-chain initialization fee before calling `commit()` or `execute()`. This cold-start requirement is specific to the legacy Solana flow and does not exist in the newer Bitcoin → Smart chain protocol used on Starknet and EVM.

## Swap Options

The overview of possible options you can pass to the swap via the `options` argument of `swapper.swap(...)`. These let you adjust watchtower incentives.

| Name                      | Type               | Description                                                                                                                                                                       |
| ------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 \| bigint` | Multiplier used when estimating the watchtower fee. Higher values make automatic settlement more attractive. Also accepts `bigint` for legacy compatibility.<br />Default: `1.5`. |

## Swap States

Read the current state of the swap in its [FromBTCSwapState](https://docs.atomiq.exchange/sdk-reference/api/atomiq-sdk/src/enumerations/FromBTCSwapState) 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 {FromBTCSwapState, SwapStateInfo} from "@atomiqlabs/sdk";



const state: FromBTCSwapState = swap.getState();

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



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

});
```

### Table of States

| State                | Value | Description                                                                                                                                     |
| -------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `FAILED`             | -4    | The Bitcoin swap address expired and the LP already refunded its Solana-side liquidity. No BTC should be sent anymore.                          |
| `EXPIRED`            | -3    | The Bitcoin swap address expired and should no longer be used, though a Bitcoin transaction already in flight might still let the swap succeed. |
| `QUOTE_EXPIRED`      | -2    | Swap quote expired and can no longer be executed.                                                                                               |
| `QUOTE_SOFT_EXPIRED` | -1    | Swap should be treated as expired, though it might still succeed if the commit transaction is already in flight.                                |
| `PR_CREATED`         | 0     | Quote created. Use `commit()` or `txsCommit()` to open the destination-side PrTLC escrow and activate the Bitcoin swap address.                 |
| `CLAIM_COMMITED`     | 1     | The Solana escrow is open and the user can send BTC with `getFundedPsbt()`, `getAddress()`, or `getHyperlink()`.                                |
| `BTC_TX_CONFIRMED`   | 2     | The Bitcoin payment has enough confirmations. Wait for watchtowers with `waitTillClaimed()` or claim manually with `claim()` / `txsClaim()`.    |
| `CLAIM_CLAIMED`      | 3     | Swap settled on Solana and funds were received.                                                                                                 |

## API Reference

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