Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 291 additions & 0 deletions api-reference/deposit-actions/bitcoin.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
---
title: 'Bitcoin'
description: 'Execute deposit transactions on Bitcoin mainnet and testnet using PSBT construction.'
---

## Overview

Bitcoin deposits use a UTXO-based model. The `call_data` from the deposit action is a **numeric memo** that must be embedded in the transaction as an `OP_RETURN` output. This memo is how Layerswap identifies and matches your deposit.

**Prerequisites:**
- [bitcoinjs-lib](https://github.com/bitcoinjs/bitcoinjs-lib) for PSBT construction
- [@bitcoinerlab/secp256k1](https://github.com/bitcoinerlab/secp256k1) for elliptic curve operations
- Access to a Bitcoin node or the [Mempool.space API](https://mempool.space/docs/api) for UTXO data and fee estimates

## call_data Format

For Bitcoin, `call_data` is a **numeric string** representing a memo identifier. Before embedding it into the transaction, convert it to a hex string:

```javascript
const hexMemo = Number(callData).toString(16);
```

This hex memo is embedded as an `OP_RETURN` output in the transaction.

## Transaction Construction

<Steps>
<Step title="Fetch UTXOs">
Retrieve unspent transaction outputs for the sender's address. You can use the Mempool.space API or your own Bitcoin node.
</Step>
<Step title="Fetch raw transactions">
For each UTXO, fetch the full raw transaction hex. This is needed to populate the `witnessUtxo` field in the PSBT inputs.
</Step>
<Step title="Select UTXOs">
Select enough UTXOs to cover the deposit amount plus estimated fees.
</Step>
<Step title="Build the PSBT">
Create a PSBT with:
- **Inputs**: Selected UTXOs with witness data
- **Output 1**: Payment to `to_address` for the deposit `amount`
- **Output 2**: `OP_RETURN` with the hex-encoded memo
- **Output 3** (if needed): Change back to the sender's address
</Step>
<Step title="Estimate fees">
Fetch the recommended fee rate from Mempool.space and calculate the transaction fee based on input/output count. Re-select UTXOs if the initial selection doesn't cover the fee.
</Step>
<Step title="Sign and broadcast">
Sign the PSBT with your private key and broadcast the raw transaction.
</Step>
</Steps>

## Full Example

```typescript
import {
Psbt,
Transaction,
networks,
opcodes,
script,
initEccLib,
payments,
} from "bitcoinjs-lib";
import * as ecc from "@bitcoinerlab/secp256k1";
import ECPairFactory from "ecpair";
import axios from "axios";

initEccLib(ecc);
const ECPair = ECPairFactory(ecc);

interface Utxo {
txid: string;
vout: number;
value: number;
status: { confirmed: boolean };
}

const MEMPOOL_BASE = {
mainnet: "https://mempool.space",
testnet: "https://mempool.space/testnet",
};

async function fetchUtxos(
address: string,
version: "mainnet" | "testnet"
): Promise<Utxo[]> {
const base = MEMPOOL_BASE[version];
const { data } = await axios.get<Utxo[]>(
`${base}/api/address/${address}/utxo`
);
return data;
}

async function fetchRawTx(
txid: string,
version: "mainnet" | "testnet"
): Promise<Transaction> {
const base = MEMPOOL_BASE[version];
const { data } = await axios.get<string>(`${base}/api/tx/${txid}/hex`);
return Transaction.fromHex(data);
}

async function fetchFeeRate(
version: "mainnet" | "testnet"
): Promise<number> {
const base = MEMPOOL_BASE[version];
const { data } = await axios.get(`${base}/api/v1/fees/recommended`);
return data.economyFee; // sats/vByte
}

function selectUtxos(
utxos: Utxo[],
target: bigint
): { selected: Utxo[]; total: bigint } {
const sorted = utxos.slice().sort((a, b) => a.value - b.value);
let sum = 0n;
const selected: Utxo[] = [];
for (const u of sorted) {
selected.push(u);
sum += BigInt(u.value);
if (sum >= target) break;
}
if (sum < target) {
throw new Error(`Insufficient funds: need ${target} sats, have ${sum}`);
}
return { selected, total: sum };
}

function estimateTxFee(
numInputs: number,
numOutputs: number,
satsPerVbyte: number
): bigint {
return BigInt((numInputs * 148 + numOutputs * 34 + 10) * satsPerVbyte);
}

async function executeBitcoinDeposit(
depositAction: any,
senderAddress: string,
senderWIF: string,
isTestnet = false
) {
const { call_data, to_address, amount } = depositAction;
const version = isTestnet ? "testnet" : "mainnet";
const network = isTestnet ? networks.testnet : networks.bitcoin;
const amountSats = Math.floor(amount * 1e8);

// Convert numeric memo to hex for OP_RETURN
const hexMemo = Number(call_data).toString(16);
const memoBuffer = Buffer.from(hexMemo, "utf8");

if (memoBuffer.length > 80) {
throw new Error("Memo too long; max 80 bytes for OP_RETURN");
}

// Fetch UTXOs and fee rate
const utxos = await fetchUtxos(senderAddress, version);
const feeRate = await fetchFeeRate(version);

// Iteratively build PSBT to account for fees
let fee = 0n;
let psbt: Psbt;
let totalSelected: bigint;

do {
const target = BigInt(amountSats) + fee;
const { selected, total } = selectUtxos(utxos, target);
totalSelected = total;

psbt = new Psbt({ network });

// Add inputs
for (const utxo of selected) {
const rawTx = await fetchRawTx(utxo.txid, version);
const out = rawTx.outs[utxo.vout];
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessUtxo: { script: out.script, value: out.value },
});
}

// Payment output
psbt.addOutput({
address: to_address,
value: BigInt(amountSats),
});

// OP_RETURN memo output
psbt.addOutput({
script: script.compile([opcodes.OP_RETURN, memoBuffer]),
value: 0n,
});

// Re-estimate fee
fee = estimateTxFee(
psbt.txInputs.length,
psbt.txOutputs.length + 1, // +1 for potential change output
feeRate
);
} while (totalSelected < BigInt(amountSats) + fee);

// Change output
const change = totalSelected - BigInt(amountSats) - fee;
if (change > 0n) {
psbt.addOutput({ address: senderAddress, value: change });
}

// Sign all inputs
const keyPair = ECPair.fromWIF(senderWIF, network);
const isTaproot =
senderAddress.startsWith("bc1p") || senderAddress.startsWith("tb1p");

for (let i = 0; i < psbt.inputCount; i++) {
if (isTaproot) {
psbt.signInput(i, keyPair, [Transaction.SIGHASH_DEFAULT]);
} else {
psbt.signInput(i, keyPair);
}
}

psbt.finalizeAllInputs();
const rawTxHex = psbt.extractTransaction().toHex();

// Broadcast via Mempool.space
const { data: txHash } = await axios.post(
`${MEMPOOL_BASE[version]}/api/tx`,
rawTxHex,
{ headers: { "Content-Type": "text/plain" } }
);

return txHash;
}
```

## Signing for Different Address Types

Bitcoin has several address formats, each with different signing requirements:

| Address Prefix | Type | Sighash |
|---|---|---|
| `1...` | Legacy (P2PKH) | `SIGHASH_ALL` (1) |
| `3...` | Nested SegWit (P2SH-P2WPKH) | `SIGHASH_ALL` (1) |
| `bc1q...` / `tb1q...` | Native SegWit (P2WPKH) | `SIGHASH_ALL` (1) |
| `bc1p...` / `tb1p...` | Taproot (P2TR) | `SIGHASH_DEFAULT` (0) |

<Warning>
Taproot addresses require `SIGHASH_DEFAULT` (0) instead of `SIGHASH_ALL` (1). Using the wrong sighash type will produce an invalid signature.
</Warning>

## Hardware / Browser Wallet Signing

If you're using a wallet provider (e.g., Xverse, Unisat, Leather) instead of a raw private key, the flow changes at the signing step. Instead of signing locally, you pass the unsigned PSBT hex to the wallet's `signPsbt` method:

```typescript
const psbtHex = psbt.toHex();
const isTaproot =
senderAddress.startsWith("bc1p") || senderAddress.startsWith("tb1p");

const signedPsbtHex = await walletProvider.request({
method: "signPsbt",
params: {
psbt: psbtHex,
inputsToSign: [
{
address: senderAddress,
signingIndexes: Array.from({ length: psbt.inputCount }, (_, i) => i),
sigHash: isTaproot ? 0 : 1,
},
],
finalize: false,
},
});

const signedPsbt = Psbt.fromHex(signedPsbtHex);
signedPsbt.finalizeAllInputs();
const rawTxHex = signedPsbt.extractTransaction().toHex();
```

## Next Step

After the transaction is submitted, notify Layerswap so it can match your deposit faster:

```bash
curl -X POST https://api.layerswap.io/api/v2/swaps/{swap_id}/deposit_speedup \
-H "X-LS-APIKEY: your_api_key" \
-H "Content-Type: application/json" \
-d '{ "transaction_id": "YOUR_TX_HASH" }'
```

See the [full deposit flow](/api-reference/deposit-actions/overview) for details.
40 changes: 40 additions & 0 deletions api-reference/deposit-actions/choosing-a-method.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: 'Choosing a deposit method'
sidebarTitle: 'Choosing a method'
description: 'The three ways to fund a swap — direct transfer, deposit address, and depository — and when to use each.'
---

When you create a swap, you choose how it will be funded. The method you pick (set on
[`POST /api/v2/swaps`](/api-reference/swaps/create-swap)) determines the
[deposit action](/api-reference/deposit-actions/overview) you get back and how you submit it.

## The three methods

| Method | Set on swap creation | How you fund the swap | Best for |
| --- | --- | --- | --- |
| **Direct transfer** | *(default — neither flag)* | Submit the returned `call_data` to the solver's address with your wallet | Simple EOA wallets sending their own funds |
| **Deposit address** | `use_deposit_address: true` | Send funds to a generated address — from any wallet or exchange (a `manual_transfer` action, no `call_data`) | Custodial flows, exchanges, or letting a user pay from anywhere |
| **Depository** | `use_depository: true` | Call Layerswap's on-chain [Depository](/api-reference/depository) contract with the prepared `call_data` | Aggregators; smart-contract, server, and programmable wallets; deterministic, batchable funding |

<Note>
`use_depository` and `use_deposit_address` are mutually exclusive — setting both is rejected.
</Note>

## When to use each

- **Direct transfer** — the simplest path when the end user signs with a normal wallet and is sending
their own funds. You get a ready-to-send transaction (`to_address` + `call_data`).
- **Deposit address** — when funds arrive from somewhere you don't build the transaction for: a
centralized exchange, a custodian, or a user "sending from anywhere." You just display the address and
amount; there is no `call_data`.
- **Depository** — the most robust path for programmatic integrations: a single contract call (with the
swap `id` baked into `call_data`) that works cleanly for smart-contract and server wallets, and lets you
batch the ERC-20 approval with the deposit. Supported on **EVM and Tron**. See
[Depository](/api-reference/depository).

## Next

- **Execute on your source chain** — per-chain guides under Network support, e.g.
[EVM](/api-reference/deposit-actions/evm).
- **Fund via the contract** — [Depository](/api-reference/depository).
- **The deposit action object & fields** — [Overview](/api-reference/deposit-actions/overview).
Loading