Bitcoin → Smart Chain
Swap Bitcoin L1 (on-chain) to Starknet or EVM tokens. These swaps are based on the UTXO-controlled vault primitive and verified through the on-chain Bitcoin light client.
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.
Solana uses a different (legacy) swap protocol. See BTC to Solana.
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 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: 0n // Optional: request native destination token as gas drop
}
); // 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());
}
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 (e.g. the built-in SingleAddressBitcoinWallet) or 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 interface or MinimalBitcoinWalletInterfaceWithSigner type to the 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: 0n
}
);
// 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() 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: 0n
}
);
// 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 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: 0n
}
);
// 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);
}
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().
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);
}
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_000_000_000_000_000_000n // Request 1 STRK as gas drop
}
);
console.log("Gas drop:", swap.getGasDropOutput().toString());
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 States
Read the current state of the swap in its SpvFromBTCSwapState enum form with getState() or in human readable SwapStateInfo form with description with 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 - Swap class for Bitcoin → Smart chain
- SwapAmountType - Amount type enum
- SpvFromBTCSwapState - Swap states