> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaleidoswap.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Move Funds Between Chains

> Swap on-chain funds from one chain to another, such as BTC to L-USDT, with a Swap SDK chain swap: lock, accept a re-quote, and claim

Your user has funds on one chain and wants them on another, for example BTC on Bitcoin to L-USDT on Liquid. This is a **chain swap**. `chainPairs()` lists the routes on offer.

## How it works

Your app creates a secret and locks funds on the source chain. The maker locks on the destination chain, to the same secret. Your app claims the maker's lockup, which reveals the secret and lets the maker claim yours. If the maker never locks, your app refunds its own lockup.

A chain swap uses two keys: a refund key for your lockup and a claim key for the maker's.

## Setup

The code on this page assumes this setup. It connects to the signet maker and loads your swap mnemonic. See [Installation](https://docs.kaleidoswap.com/sdk/installation.md) and [Keys and recovery](https://docs.kaleidoswap.com/sdk/keys-and-recovery.md) if you have neither yet.

```typescript TypeScript theme={null}
import { init, SwapClient, SwapMasterKey, SwapScript, toJson } from "@kaleidorg/swap-sdk";

await init();

const network = "signet";
const makerUrl = "https://maker.signet.kaleidoswap.com/v2";
const bitcoinEsploraUrl = "https://esplora.signet.kaleidoswap.com";
const liquidEsploraUrl = "https://blockstream.info/liquidtestnet/api";

const client = new SwapClient(makerUrl);
const master = SwapMasterKey.fromSwapMnemonic(process.env.SWAP_MNEMONIC!, network);

// Two things your app provides:
//   nextSwapIndex(): Promise<bigint>  a persisted counter, never reused
//   store                             durable storage for swap records
```

```python Python theme={null}
import asyncio, os
import kaleidorg_swap_sdk as sdk

network = sdk.Network.SIGNET
client = sdk.SwapClient("https://maker.signet.kaleidoswap.com/v2", None)
master = sdk.SwapMasterKey.from_swap_mnemonic(os.environ["SWAP_MNEMONIC"], network)
```

## Swap

**1. Create the swap**

```typescript theme={null}
const refundIndex = await nextSwapIndex();
const claimIndex = await nextSwapIndex();
const refundKey = master.deriveSwapKey(refundIndex);
const claimKey = master.deriveSwapKey(claimIndex);
const preimage = master.derivePreimage(claimIndex);

const card = (await client.chainPairs())?.BTC?.["L-USDT"];
if (!card) throw new Error("Route not offered right now");

const swap = await client.createChainSwap(network, {
  from: "BTC",
  to: "L-USDT",
  preimageHash: preimage.sha256,
  claimPublicKey: claimKey.publicKey,
  refundPublicKey: refundKey.publicKey,
  userLockAmount: 100_000n,
  pairHash: card.hash,
});

await store.put(swap.id, toJson({ refundIndex, claimIndex, makerUrl, swap }));
```

The response has `lockupDetails` (where you lock) and `claimDetails` (where the maker locks for you). The SDK checks both against your keys.

**2. Lock**

Send exactly the expected amount to `swap.lockupDetails.lockupAddress` in one transaction.

**3. Wait for the maker's lockup**

The swap reaches `transaction.server.mempool`, then `transaction.server.confirmed`.

**4. Claim the L-USDT**

L-USDT pays its Liquid fee in L-BTC, so the claim is a **caller-funded PSET**: the SDK prepares a partial transaction, your Liquid wallet adds an L-BTC input for the fee and signs it, and the SDK finalizes it with your secret.

```typescript theme={null}
// Both sides rebuild from the response; a mismatch throws here.
const lockupScript = SwapScript.fromChain("bitcoin", network, "lockup", swap.lockupDetails, refundKey.publicKey);
const claimScript = SwapScript.fromChain("liquid", network, "claim", swap.claimDetails, claimKey.publicKey);

const prepared = await claimScript.prepareLiquidClaim({
  outputAddress: myExplicitLiquidAddress, // an unconfidential Liquid address
  swapId: swap.id,
  maxFee: 1_000n,                         // your fee ceiling, in L-BTC sats
  quotedFeeCap: 1_000n,                   // the claim fee the pair card quoted; the lower cap wins
  makerBaseUrl: makerUrl,
  network,
  liquidEsploraUrl,
});

// Your Liquid wallet funds the fee with L-BTC, blinds, and signs its own input.
const funded = await myLiquidWallet.fundAndSign(prepared.template());

const claim = prepared.finalizeClaim(funded, claimKey.secretKey, preimage.preimage);
// Save claim.hex() first, then:
await claim.broadcast(network, undefined, liquidEsploraUrl);
```

`myLiquidWallet.fundAndSign` stands for your wallet's code: it takes the template's `pset`, adds and signs an L-BTC input that covers the fee, and returns `{ pset, paymentOutputSecrets }`. The swap ends in `transaction.claimed`.

For a chain swap that pays out L-BTC or BTC instead, claim with `claimScript.constructCooperativeClaim(preimage, params, lockupScript, refundKey.secretKey)`, which signs with your **refund** key against the lockup script, and fall back to `constructClaim(preimage, { ...params, cooperative: false })` if the maker does not cooperate.

## Accept a re-quote

If you locked a different amount than quoted, the maker can offer a new destination amount. Accepting it needs the swap's `swapAuth`:

```typescript theme={null}
const quote = await client.quote(swap.id);
await client.acceptQuote(swap.id, quote.amount, swap.swapAuth);
```

Without `swapAuth` the call fails with `401 invalid_swap_auth`, and the swap expires into its refund.

## If something goes wrong

Refund your lockup with `lockupScript.constructRefund({ ...params, swapAuth: swap.swapAuth })`, or let `refundChainSwap` choose between the cooperative and script paths. Your refund uses the **source** chain's deadline; your claim uses the **destination** chain's.

> [!NOTE]
>
> USDT-RGB is not a chain-swap asset. For BTC ↔ USDT-RGB on chain, use an [atomic swap](https://docs.kaleidoswap.com/sdk/rgb-swaps.md#atomic-swaps-on-l1).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.