diff --git a/api-reference/deposit-actions/bitcoin.mdx b/api-reference/deposit-actions/bitcoin.mdx new file mode 100644 index 0000000..ca508bb --- /dev/null +++ b/api-reference/deposit-actions/bitcoin.mdx @@ -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 + + + + Retrieve unspent transaction outputs for the sender's address. You can use the Mempool.space API or your own Bitcoin node. + + + For each UTXO, fetch the full raw transaction hex. This is needed to populate the `witnessUtxo` field in the PSBT inputs. + + + Select enough UTXOs to cover the deposit amount plus estimated fees. + + + 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 + + + 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. + + + Sign the PSBT with your private key and broadcast the raw transaction. + + + +## 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 { + const base = MEMPOOL_BASE[version]; + const { data } = await axios.get( + `${base}/api/address/${address}/utxo` + ); + return data; +} + +async function fetchRawTx( + txid: string, + version: "mainnet" | "testnet" +): Promise { + const base = MEMPOOL_BASE[version]; + const { data } = await axios.get(`${base}/api/tx/${txid}/hex`); + return Transaction.fromHex(data); +} + +async function fetchFeeRate( + version: "mainnet" | "testnet" +): Promise { + 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) | + + + Taproot addresses require `SIGHASH_DEFAULT` (0) instead of `SIGHASH_ALL` (1). Using the wrong sighash type will produce an invalid signature. + + +## 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. \ No newline at end of file diff --git a/api-reference/deposit-actions/choosing-a-method.mdx b/api-reference/deposit-actions/choosing-a-method.mdx new file mode 100644 index 0000000..83c7498 --- /dev/null +++ b/api-reference/deposit-actions/choosing-a-method.mdx @@ -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 | + + + `use_depository` and `use_deposit_address` are mutually exclusive — setting both is rejected. + + +## 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). diff --git a/api-reference/deposit-actions/evm.mdx b/api-reference/deposit-actions/evm.mdx new file mode 100644 index 0000000..0d0c5b5 --- /dev/null +++ b/api-reference/deposit-actions/evm.mdx @@ -0,0 +1,187 @@ +--- +title: 'EVM Chains' +description: 'Execute deposit transactions on Ethereum, Arbitrum, Optimism, Base, Polygon, and other EVM-compatible networks.' +--- + +import DepositoryDepositEvm from "/snippets/depository-deposit-evm.mdx"; + +## Overview + +This guide covers all EVM-compatible networks supported by Layerswap, including Ethereum, Arbitrum, Optimism, Base, Polygon, Linea, zkSync Era, Scroll, and others. + +**Prerequisites:** +- [viem](https://viem.sh/) (recommended) or [ethers.js](https://docs.ethers.org/) +- A wallet client or signer with funds on the source network + +## call_data Format + +For EVM chains, `call_data` is a **hex-encoded string** (`0x...`) representing the transaction's `data` field. It always carries a Layerswap memo (a fixed prefix followed by a sequence number) that the backend uses to match the deposit: + +- **Native token transfers** (ETH, MATIC, etc.): `call_data` is the memo hex. `to_address` is the deposit address and `amount` / `amount_in_base_units` carry the value to send. +- **ERC-20 token transfers**: `call_data` is the encoded `transfer(address,uint256)` function call with the memo hex appended. `to_address` is the **token contract**, and `amount` / `amount_in_base_units` are `0` — the actual transfer value is encoded inside `call_data`. + +In both cases, build the transaction with `to: to_address`, `value: amount`, and `data: call_data` and the wallet will route it correctly. + +## Transaction Construction + + + + Extract `call_data`, `to_address`, `amount`, and `network.chain_id` from the deposit action. + + + Construct a transaction object with the deposit action fields mapped to standard EVM transaction parameters. + + + Call `eth_estimateGas` for a more accurate gas limit. If estimation fails, the transaction can still be sent without an explicit gas limit — the wallet or node will estimate it. + + + Sign and broadcast the transaction, then return the hash. + + + +## Full Example + +Pick the library that fits your stack. The `viem` and `ethers.js` tabs cover server-side or Node.js usage. The `wagmi` tab is for browser-side use with a connected wallet. + + +```typescript viem +import { createWalletClient, createPublicClient, http, parseEther } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { mainnet, arbitrum, optimism, base, polygon } from "viem/chains"; + +const CHAIN_MAP = { + 1: mainnet, + 42161: arbitrum, + 10: optimism, + 8453: base, + 137: polygon, + // Add other EVM chains as needed +}; + +async function executeEvmDeposit(depositAction, privateKey) { + const { call_data, to_address, amount, network } = depositAction; + const chainId = Number(network.chain_id); + const chain = CHAIN_MAP[chainId]; + + if (!chain) { + throw new Error(`Unsupported chain ID: ${chainId}`); + } + + const account = privateKeyToAccount(privateKey); + + const walletClient = createWalletClient({ + account, + chain, + transport: http(network.node_url), + }); + + const publicClient = createPublicClient({ + chain, + transport: http(network.node_url), + }); + + // Estimate gas + let gas; + try { + gas = await publicClient.estimateGas({ + account: account.address, + to: to_address, + value: parseEther(amount.toString()), + data: call_data, + }); + } catch (error) { + console.warn("Gas estimation failed, proceeding without explicit gas limit"); + } + + // Send transaction + const hash = await walletClient.sendTransaction({ + to: to_address, + value: parseEther(amount.toString()), + data: call_data, + gas, + }); + + return hash; +} +``` + +```typescript ethers.js +import { ethers } from "ethers"; + +async function executeEvmDeposit(depositAction, privateKey) { + const { call_data, to_address, amount, network } = depositAction; + + const provider = new ethers.JsonRpcProvider(network.node_url); + const wallet = new ethers.Wallet(privateKey, provider); + + // Estimate gas + let gasLimit; + try { + gasLimit = await provider.estimateGas({ + from: wallet.address, + to: to_address, + value: ethers.parseEther(amount.toString()), + data: call_data, + }); + } catch (error) { + console.warn("Gas estimation failed, proceeding without explicit gas limit"); + } + + // Send transaction + const tx = await wallet.sendTransaction({ + to: to_address, + value: ethers.parseEther(amount.toString()), + data: call_data, + gasLimit, + }); + + return tx.hash; +} +``` + +```typescript wagmi +import { sendTransaction } from "@wagmi/core"; +import { parseEther } from "viem"; + +async function executeEvmDeposit(depositAction, wagmiConfig) { + const { call_data, to_address, amount, network } = depositAction; + + const hash = await sendTransaction(wagmiConfig, { + chainId: Number(network.chain_id), + to: to_address, + value: parseEther(amount.toString()), + data: call_data, + }); + + return hash; +} +``` + + + + When using a browser wallet ([wagmi](https://wagmi.sh/)), ensure the user is connected to the correct chain (`network.chain_id`). If not, prompt them to switch networks before sending the transaction. + + +## Funding via the Depository + +The example above covers the **direct transfer** and **deposit address** methods. If you created the swap +with `use_depository: true`, the deposit action targets the on-chain +[Depository](/api-reference/depository) contract instead — submit its `call_data`, and for ERC-20 approve +the contract first: + + + +See [Choosing a method](/api-reference/deposit-actions/choosing-a-method) for when to use each. + +## 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, or the [Depository](/api-reference/depository) guide to fund via the on-chain contract (`use_depository`). \ No newline at end of file diff --git a/api-reference/deposit-actions/fuel.mdx b/api-reference/deposit-actions/fuel.mdx new file mode 100644 index 0000000..4ce3a8e --- /dev/null +++ b/api-reference/deposit-actions/fuel.mdx @@ -0,0 +1,200 @@ +--- +title: 'Fuel' +description: 'Execute deposit transactions on the Fuel network using pre-built script transactions from call_data.' +--- + +## Overview + +Fuel deposits use a script-based transaction model. The Layerswap API returns a **pre-built script transaction** in `call_data` that you need to deserialize, fund with the required coin quantities, and submit. + +**Prerequisites:** +- [fuels](https://docs.fuel.network/docs/fuels-ts/) (Fuel TypeScript SDK) for transaction construction and wallet interaction + +**Supported networks:** Fuel Mainnet, Fuel Testnet + +## call_data Format + +For Fuel, `call_data` is a **JSON string** containing two fields: + +```json +{ + "script": { /* ScriptTransactionRequest object */ }, + "quantities": [ + { "amount": "1000000", "assetId": "0x..." } + ] +} +``` + +- `script` — A serialized `ScriptTransactionRequest` object containing the transaction script, inputs, outputs, and other parameters. +- `quantities` — An array of coin quantities required to fund the transaction (covers the transfer amount and fees). + +## Transaction Construction + + + + Deserialize the JSON string and extract the `script` and `quantities` fields. + + + Use `ScriptTransactionRequest.from()` to create a proper transaction request object from the serialized data. + + + Call `estimateAndFund()` on the transaction with the wallet and required quantities. This estimates gas costs and adds the necessary coin inputs. + + + Simulate the transaction against the Fuel provider to verify it will succeed before sending. + + + Submit the transaction through the Fuel wallet and get the transaction ID. + + + +## Full Example + +Pick the flavor that matches your setup. The `Server-side` tab signs with a raw private key. The `Browser` tab uses the Fuel Wallet extension via the Fuel connector. The `@fuels/react` tab wraps it as a hook for React apps. + + +```typescript Server-side +import { + Provider, + Wallet, + ScriptTransactionRequest, + coinQuantityfy, +} from "fuels"; + +async function executeFuelDeposit( + depositAction: any, + privateKey: string +) { + const { call_data, network } = depositAction; + + // Connect to the Fuel provider + const provider = new Provider(network.node_url); + const wallet = Wallet.fromPrivateKey(privateKey, provider); + + // Parse the call_data + const parsedCallData = JSON.parse(call_data); + + // Reconstruct the script transaction + const scriptTransaction = ScriptTransactionRequest.from( + parsedCallData.script + ); + + // Parse coin quantities + const quantities = parsedCallData.quantities.map((q: any) => + coinQuantityfy(q) + ); + + // Estimate gas and fund the transaction with required coins + await scriptTransaction.estimateAndFund(wallet, { quantities }); + + // Simulate to verify (optional but recommended) + await provider.simulate(scriptTransaction); + + // Send the transaction + const response = await wallet.sendTransaction(scriptTransaction); + + return response.id; +} +``` + +```typescript Browser +import { + Provider, + ScriptTransactionRequest, + coinQuantityfy, +} from "fuels"; + +async function executeFuelDeposit( + depositAction: any, + fuel: any, // Fuel instance from @fuels/react + senderAddress: string +) { + const { call_data, network } = depositAction; + + const provider = new Provider(network.node_url); + const wallet = await fuel.getWallet(senderAddress, provider); + + if (!wallet) { + throw new Error("Fuel wallet not found"); + } + + const parsedCallData = JSON.parse(call_data); + const scriptTransaction = ScriptTransactionRequest.from( + parsedCallData.script + ); + const quantities = parsedCallData.quantities.map((q: any) => + coinQuantityfy(q) + ); + + await scriptTransaction.estimateAndFund(wallet, { quantities }); + + // Simulate first + await provider.simulate(scriptTransaction); + + // Send through the browser wallet + const response = await wallet.sendTransaction(scriptTransaction); + + return response.id; +} +``` + +```typescript @fuels/react +import { useFuel } from "@fuels/react"; +import { + Provider, + ScriptTransactionRequest, + coinQuantityfy, +} from "fuels"; + +function useFuelDeposit() { + const { fuel } = useFuel(); + + async function executeDeposit( + depositAction: any, + senderAddress: string + ): Promise { + if (!fuel) { + throw new Error("Fuel not initialized"); + } + + const { call_data, network } = depositAction; + + const provider = new Provider(network.node_url); + const wallet = await fuel.getWallet(senderAddress, provider); + + if (!wallet) { + throw new Error("Fuel wallet not found"); + } + + const parsedCallData = JSON.parse(call_data); + const scriptTransaction = ScriptTransactionRequest.from( + parsedCallData.script + ); + const quantities = parsedCallData.quantities.map((q: any) => + coinQuantityfy(q) + ); + + await scriptTransaction.estimateAndFund(wallet, { quantities }); + await provider.simulate(scriptTransaction); + + const response = await wallet.sendTransaction(scriptTransaction); + return response.id; + } + + return { executeDeposit }; +} +``` + + +## 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. \ No newline at end of file diff --git a/api-reference/deposit-actions/overview.mdx b/api-reference/deposit-actions/overview.mdx new file mode 100644 index 0000000..5c229de --- /dev/null +++ b/api-reference/deposit-actions/overview.mdx @@ -0,0 +1,163 @@ +--- +title: 'Deposit Actions' +sidebarTitle: 'Overview' +description: 'Execute the on-chain deposit that funds a swap — the deposit action object, the funding modes, and the per-chain execution guides.' +--- + +## Overview + +When you create a swap via [`POST /api/v2/swaps`](/api-reference/swaps/create-swap), the response includes +a `deposit_actions` array. A **deposit action** is the exact on-chain transaction to submit on the +**source** chain to fund the swap: a target `to_address`, an `amount`, and a blockchain-specific +`call_data` payload. Submit it and Layerswap detects the deposit and delivers the funds on the +destination chain. + +This page is the reference for the deposit action object and the funding modes. The per-chain guides +below show how to build and submit the transaction on each blockchain. For the end-to-end flow +(discover → quote → create → execute → track), see the [API Integration](/integration/API) guide. + +## Funding modes + +The method you choose when creating the swap determines the deposit action you receive: + +- **Direct transfer** (default) and **Depository** (`use_depository: true`) return a `type: "transfer"` + action with `call_data` to submit (Depository also adds `gas_limit` and `encoded_args`). +- **Deposit address** (`use_deposit_address: true`) returns a `type: "manual_transfer"` action — a + generated address with `call_data: null`. + +See [Choosing a method](/api-reference/deposit-actions/choosing-a-method) for the full comparison and when +to use each. + + + `use_depository` and `use_deposit_address` are mutually exclusive. Some networks support only a subset + of modes (e.g. Starknet and TON support only the transfer flow) — see each chain guide. + + +## Deposit action schema + +Each item in the `deposit_actions` array has the following structure: + + + Either `transfer` (submit a transaction built from `call_data`) or `manual_transfer` (send funds to a + generated deposit address; `call_data` is `null`). + + + + The transaction target — the solver address, a generated deposit address, or the Depository contract, + depending on the funding mode. + + + + The amount to send in human-readable units (e.g. `0.1` ETH, not wei). `0` for ERC20 depository deposits + (the amount is carried inside `call_data`). + + + + The amount in the token's smallest unit (wei for ETH, satoshi for BTC, …). For native transfers, send + this as the transaction value. + + + + Blockchain-specific payload used to construct the transaction. `null` for `manual_transfer`. The format + varies by chain — see the guides below. + + + + The source network object, including `name`, `type`, `chain_id`, and `node_url`. + + + + The token being deposited, including `symbol`, `decimals`, and `contract` (null for native tokens). + + + + The token used to pay transaction fees on this network. + + + + Execution order. There is normally a single action; if more than one is returned, execute them in + ascending `order`. + + + + Suggested gas limit for the contract call. Returned only for **Depository** deposits. + + + + The individual, decoded contract-call arguments, for integrators who prefer to build the call + themselves. Returned only for **Depository** deposits. + + +## Step-by-step flow + + + + Call [`POST /api/v2/swaps`](/api-reference/swaps/create-swap). The response contains the + `deposit_actions` array. + + ```bash + curl -X POST https://api.layerswap.io/api/v2/swaps \ + -H "X-LS-APIKEY: $LAYERSWAP_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "source_network": "ETHEREUM_MAINNET", + "source_token": "ETH", + "destination_network": "ARBITRUM_MAINNET", + "destination_token": "ETH", + "destination_address": "0xRecipient", + "source_address": "0xSender", + "amount": 0.1, + "use_deposit_address": false + }' + ``` + + + Take the first action (or iterate in `order`): + + ```javascript + const action = swap.deposit_actions[0]; + const { type, to_address, amount_in_base_units, call_data, network, token } = action; + ``` + + + Build and broadcast the transaction on the source chain using `call_data`, `to_address`, and the + amount. The mechanics differ per chain — follow the matching guide below (and + [Depository](/api-reference/depository) for depository deposits). + + + Submit the returned `call_data` **verbatim**. It encodes the swap `id` and the assigned receiver; + altering it means Layerswap can't match your deposit to the swap. + + + + Report the transaction hash so Layerswap matches it without waiting for routine polling: + + ```bash + curl -X POST https://api.layerswap.io/api/v2/swaps/{swap_id}/deposit_speedup \ + -H "X-LS-APIKEY: $LAYERSWAP_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "transaction_id": "0xYourTransactionHash" }' + ``` + + + Poll [`GET /api/v2/swaps/{swapId}`](/api-reference/swaps/get-swap-details) and follow the + [swap lifecycle](/api-reference/swap-lifecycle) to completion. + + + +## call_data format by chain + +The `call_data` format depends on the source blockchain: + +| Blockchain | call_data format | Guide | +| --- | --- | --- | +| EVM (Ethereum, Arbitrum, Base, …) | Hex-encoded transaction data (`0x…`) | [EVM](/api-reference/deposit-actions/evm) | +| Bitcoin | Numeric memo, embedded as `OP_RETURN` | [Bitcoin](/api-reference/deposit-actions/bitcoin) | +| Solana | Base64-encoded serialized transaction | [Solana](/api-reference/deposit-actions/solana) | +| Starknet | JSON array of Starknet calls | [Starknet](/api-reference/deposit-actions/starknet) | +| TON | JSON object with `comment` (and `amount` for Jettons) | [TON](/api-reference/deposit-actions/ton) | +| Tron | Memo, hex-encoded into the transaction data | [Tron](/api-reference/deposit-actions/tron) | +| Fuel | JSON object with `script` and `quantities` | [Fuel](/api-reference/deposit-actions/fuel) | + +For funding via the on-chain contract (`use_depository: true`), see the +[Depository](/api-reference/depository) guide. diff --git a/api-reference/deposit-actions/solana.mdx b/api-reference/deposit-actions/solana.mdx new file mode 100644 index 0000000..5c8e750 --- /dev/null +++ b/api-reference/deposit-actions/solana.mdx @@ -0,0 +1,197 @@ +--- +title: 'Solana' +description: 'Execute deposit transactions on Solana by deserializing and signing the pre-built transaction from call_data.' +--- + +## Overview + +Solana deposits are straightforward because the Layerswap API returns a **fully constructed, serialized transaction** in `call_data`. You only need to decode it, set a fresh blockhash, sign it, and send it. + +**Prerequisites:** +- [@solana/web3.js](https://github.com/solana-foundation/solana-web3.js) for transaction handling and RPC communication + +## call_data Format + +For Solana, `call_data` is a **base64-encoded serialized `Transaction`**. It contains all the instructions, accounts, and program IDs already set up by the Layerswap backend — including SPL token transfers when applicable. + +```javascript +// Node.js / server-side +const buffer = Buffer.from(callData, "base64"); +const transaction = Transaction.from(buffer); + +// Browser +const buffer = Uint8Array.from(atob(callData), (c) => c.charCodeAt(0)); +const transaction = Transaction.from(buffer); +``` + +## Transaction Construction + + + + Convert the base64 `call_data` string into a `Transaction` object. + + + Fetch the latest blockhash from the Solana RPC and update the transaction. This ensures the transaction doesn't expire before it's confirmed. + + + Estimate the transaction fee and verify the sender has enough SOL for fees and enough of the source token for the transfer amount. + + + Sign the transaction with the sender's keypair or wallet adapter. + + + Submit the signed transaction to the network and wait for confirmation. + + + +## Full Example + +The `Server-side` tab signs locally with a `Keypair`. The `Browser` tab uses a connected wallet adapter (Phantom, Solflare, etc.) and its `signTransaction` method. + + +```typescript Server-side +import { + Connection, + Transaction, + Keypair, + LAMPORTS_PER_SOL, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import bs58 from "bs58"; + +async function executeSolanaDeposit( + depositAction: any, + senderKeypair: Keypair +) { + const { call_data, network } = depositAction; + + const connection = new Connection(network.node_url, "confirmed"); + + // Decode base64 call_data into a Transaction + const buffer = Buffer.from(call_data, "base64"); + const transaction = Transaction.from(buffer); + + // Set a fresh blockhash + const { blockhash, lastValidBlockHeight } = + await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.lastValidBlockHeight = lastValidBlockHeight; + + // Send and confirm + const signature = await sendAndConfirmTransaction( + connection, + transaction, + [senderKeypair], + { commitment: "confirmed" } + ); + + return signature; +} +``` + +```typescript Browser +import { Connection, Transaction, LAMPORTS_PER_SOL } from "@solana/web3.js"; + +async function executeSolanaDeposit( + depositAction: any, + nodeUrl: string, + signTransaction: (tx: Transaction) => Promise +) { + const { call_data } = depositAction; + + const connection = new Connection(nodeUrl, "confirmed"); + + // Decode base64 call_data + const buffer = Uint8Array.from(atob(call_data), (c) => c.charCodeAt(0)); + const transaction = Transaction.from(buffer); + + + // Set fresh blockhash + const blockHash = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockHash.blockhash; + transaction.lastValidBlockHeight = blockHash.lastValidBlockHeight; + + // Sign with wallet adapter + const signed = await signTransaction(transaction); + + // Send the signed transaction + const rawTransaction = signed.serialize(); + const txid = await connection.sendRawTransaction(rawTransaction, { + skipPreflight: true, + }); + + // Confirm + await connection.confirmTransaction( + { + signature: txid, + blockhash: blockHash.blockhash, + lastValidBlockHeight: blockHash.lastValidBlockHeight, + }, + "confirmed" + ); + + return txid; +} +``` + + +## Sending with Retry Logic + +Solana transactions can sometimes fail to land due to network congestion. A robust implementation resends the transaction periodically until it's confirmed or the blockhash expires: + +```typescript +import { Connection, Transaction } from "@solana/web3.js"; + +async function sendWithRetry( + connection: Connection, + serializedTx: Buffer, + blockhash: string, + lastValidBlockHeight: number +): Promise { + const txid = await connection.sendRawTransaction(serializedTx, { + skipPreflight: true, + }); + + const controller = new AbortController(); + + // Resend every 2 seconds in the background + const resendLoop = async () => { + while (!controller.signal.aborted) { + await new Promise((r) => setTimeout(r, 2000)); + if (controller.signal.aborted) return; + try { + await connection.sendRawTransaction(serializedTx, { + skipPreflight: true, + }); + } catch { + // Ignore resend errors + } + } + }; + resendLoop(); + + try { + await connection.confirmTransaction( + { signature: txid, blockhash, lastValidBlockHeight }, + "confirmed" + ); + } finally { + controller.abort(); + } + + return txid; +} +``` + +## 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. \ No newline at end of file diff --git a/api-reference/deposit-actions/starknet.mdx b/api-reference/deposit-actions/starknet.mdx new file mode 100644 index 0000000..f0541c4 --- /dev/null +++ b/api-reference/deposit-actions/starknet.mdx @@ -0,0 +1,153 @@ +--- +title: 'Starknet' +description: 'Execute deposit transactions on Starknet using the multicall format from call_data.' +--- + +## Overview + +Starknet deposits are simple to execute because the Layerswap API returns a **pre-built array of Starknet calls** in `call_data`. You parse the JSON and pass it directly to the account's `execute` method, which handles multicall execution natively. + +**Prerequisites:** +- [starknet.js](https://github.com/starknet-io/starknet.js) for account interaction and transaction execution + +**Supported networks:** Starknet Mainnet, Starknet Sepolia + +## call_data Format + +For Starknet, `call_data` is a **JSON-encoded array of call objects**. Layerswap always returns **two calls**: + +1. A `transfer` call on the token contract (ETH on Starknet is also an ERC-20). +2. A `watch` call on the Layerswap **Watchdog** contract, which the backend uses to match the deposit. + +```json +[ + { + "contractAddress": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + "entrypoint": "transfer", + "calldata": ["0xRecipient", "0xAmountLow", "0xAmountHigh"] + }, + { + "contractAddress": "0xWatchdogContract...", + "entrypoint": "watch", + "calldata": ["0xSequenceNumber"] + } +] +``` + +Both calls must execute atomically — pass the whole array to `account.execute()` as shown below; Starknet handles them as a single multicall transaction. + +## Transaction Construction + + + + Deserialize the JSON string into an array of Starknet call objects. + + + Pass the calls array to `account.execute()`. Starknet natively supports multicall, so all calls in the array are executed atomically in a single transaction. + + + The `execute` method returns an object with `transaction_hash`. + + + +## Full Example + +Pick the flavor that matches your setup. The `Server-side` tab signs with a raw private key via `starknet.js`. The `Browser` tab connects to an injected wallet like ArgentX or Braavos via `get-starknet`. The `starknet-react` tab wraps it as a hook for React apps. + + +```typescript Server-side +import { Account, RpcProvider, Call } from "starknet"; + +async function executeStarknetDeposit( + depositAction: any, + senderAddress: string, + privateKey: string +) { + const { call_data, network } = depositAction; + + const provider = new RpcProvider({ nodeUrl: network.node_url }); + const account = new Account(provider, senderAddress, privateKey); + + // Parse the call_data JSON into Starknet calls + const calls: Call[] = JSON.parse(call_data); + + // Execute the multicall transaction + const { transaction_hash } = await account.execute(calls); + + if (!transaction_hash) { + throw new Error("No transaction hash returned"); + } + + // Optionally wait for confirmation + await provider.waitForTransaction(transaction_hash); + + return transaction_hash; +} +``` + +```typescript Browser +import { connect } from "get-starknet"; +import { Call } from "starknet"; + +async function executeStarknetDeposit(depositAction: any) { + const { call_data } = depositAction; + + // Connect to wallet + const starknet = await connect(); + if (!starknet?.isConnected || !starknet.account) { + throw new Error("Starknet wallet not connected"); + } + + // Parse the call_data JSON + const calls: Call[] = JSON.parse(call_data); + + // Execute through the connected wallet + const { transaction_hash } = await starknet.account.execute(calls); + + if (!transaction_hash) { + throw new Error("No transaction hash returned"); + } + + return transaction_hash; +} +``` + +```typescript starknet-react +import { useAccount } from "@starknet-react/core"; +import { Call } from "starknet"; + +function useStarknetDeposit() { + const { account } = useAccount(); + + async function executeDeposit(depositAction: any): Promise { + if (!account) { + throw new Error("Starknet account not connected"); + } + + const calls: Call[] = JSON.parse(depositAction.call_data); + const { transaction_hash } = await account.execute(calls); + + if (!transaction_hash) { + throw new Error("No transaction hash returned"); + } + + return transaction_hash; + } + + return { executeDeposit }; +} +``` + + +## 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. \ No newline at end of file diff --git a/api-reference/deposit-actions/ton.mdx b/api-reference/deposit-actions/ton.mdx new file mode 100644 index 0000000..3228a71 --- /dev/null +++ b/api-reference/deposit-actions/ton.mdx @@ -0,0 +1,261 @@ +--- +title: 'TON' +description: 'Execute deposit transactions on The Open Network for both native TON and Jetton (token) transfers.' +--- + +## Overview + +TON deposits require constructing a TON-specific message payload. The flow differs depending on whether you're transferring **native TON** or a **Jetton** (TRC-20-like token on TON). The `call_data` from the Layerswap API contains the information needed to build the correct payload. + +**Prerequisites:** +- [@ton/ton](https://github.com/ton-org/ton) for cell building and TON client +- [@tonconnect/ui-react](https://github.com/ton-connect/sdk) (for browser wallets) or a TON SDK for server-side signing + +## call_data Format + +For TON, `call_data` is a **JSON string** containing: + +```json +{ + "amount": "1000000000", + "asset": "TON", + "comment": "layerswap_memo_identifier" +} +``` + +- `comment` — A text memo that Layerswap uses to identify the deposit. This is embedded in the transaction payload. +- `amount` — The transfer amount in base units (nanotons for TON, base units of the Jetton otherwise). Always present; for native TON it matches the deposit action's `amount` field. +- `asset` — The source asset symbol (e.g. `TON`, `USDT`). Informational; the actual contract is on the deposit action's `token.contract`. + +## Native TON Transfer + +For native TON transfers (when `token.contract` is null), the transaction is a simple message to the deposit address with a comment payload. + +### Transaction Construction + + + + Extract the `comment` field from the JSON. + + + Create a TON cell with a 32-bit zero prefix (indicates a text comment) followed by the comment string. + + + Send a message to `to_address` with the deposit `amount` (converted to nanotons) and the comment cell as payload. + + + +### Full Example + +```typescript +import { beginCell, toNano } from "@ton/ton"; + +function buildNativeTonTransaction( + depositAction: any +) { + const { call_data, to_address, amount } = depositAction; + const { comment } = JSON.parse(call_data); + + const body = beginCell() + .storeUint(0, 32) // 0 opcode = text comment + .storeStringTail(comment) + .endCell(); + + return { + validUntil: Math.floor(Date.now() / 1000) + 360, + messages: [ + { + address: to_address, + amount: toNano(amount).toString(), + payload: body.toBoc().toString("base64"), + }, + ], + }; +} +``` + +## Jetton Transfer + +For Jetton transfers (when `token.contract` is not null), you need to build a Jetton transfer message that targets the sender's Jetton wallet address (not the Jetton master contract). + +### Transaction Construction + + + + Extract both `comment` and `amount` from the JSON. + + + Use the Jetton master contract to look up the sender's Jetton wallet address via `get_wallet_address`. + + + Construct a cell with the Jetton transfer opcode (`0x0f8a7ea5`), the Jetton amount, the destination address, and a forward payload containing the comment. + + + Send the message to the sender's Jetton wallet address with enough TON attached to cover fees. + + + +### Full Example + +```typescript +import { + Address, + JettonMaster, + TonClient, + beginCell, + toNano, +} from "@ton/ton"; + +async function buildJettonTransaction( + depositAction: any, + senderAddress: string, + tonClient: TonClient +) { + const { call_data, to_address, token } = depositAction; + const { comment, amount: jettonAmount } = JSON.parse(call_data); + + const destinationAddress = Address.parse(to_address); + const userAddress = Address.parse(senderAddress); + + // Build the forward payload (comment) + const forwardPayload = beginCell() + .storeUint(0, 32) + .storeStringTail(comment) + .endCell(); + + // Build the Jetton transfer body + const body = beginCell() + .storeUint(0x0f8a7ea5, 32) // Jetton transfer opcode + .storeUint(0, 64) // query_id + .storeCoins(BigInt(jettonAmount)) // Jetton amount in base units + .storeAddress(destinationAddress) // destination + .storeAddress(destinationAddress) // response excess destination + .storeBit(0) // no custom payload + .storeCoins(toNano("0.00002")) // forward amount for notification + .storeBit(1) // forward payload as reference + .storeRef(forwardPayload) + .endCell(); + + // Resolve the sender's Jetton wallet address + const jettonMasterAddress = Address.parse(token.contract); + const jettonMaster = tonClient.open( + JettonMaster.create(jettonMasterAddress) + ); + const jettonWalletAddress = + await jettonMaster.getWalletAddress(userAddress); + + return { + validUntil: Math.floor(Date.now() / 1000) + 360, + messages: [ + { + address: jettonWalletAddress.toString(), + amount: toNano("0.045").toString(), // TON for fees; excess is returned + payload: body.toBoc().toString("base64"), + }, + ], + }; +} +``` + +## Sending the Transaction + +The `TON Connect` tab is for browser wallets like Tonkeeper or MyTonWallet — it picks between the native and Jetton builders above based on `token.contract`. The `Server-side` tab signs and sends directly with a wallet key for backend use. + + +```typescript TON Connect +import { TonConnectUI } from "@tonconnect/ui-react"; +import { TonClient } from "@ton/ton"; + +async function executeTonDeposit( + depositAction: any, + senderAddress: string, + tonConnectUI: TonConnectUI, + tonClient: TonClient +) { + const { token } = depositAction; + let transaction; + + if (token.contract) { + transaction = await buildJettonTransaction( + depositAction, + senderAddress, + tonClient + ); + } else { + transaction = buildNativeTonTransaction(depositAction); + } + + const result = await tonConnectUI.sendTransaction(transaction); + + // result.boc contains the sent message BOC + // You can use it to track the transaction on-chain + return result.boc; +} +``` + +```typescript Server-side +import { + WalletContractV4, + internal, + TonClient, + beginCell, + toNano, +} from "@ton/ton"; +import { mnemonicToPrivateKey } from "@ton/crypto"; + +async function executeTonDepositServerSide( + depositAction: any, + mnemonic: string[] +) { + const { call_data, to_address, amount, network } = depositAction; + const { comment } = JSON.parse(call_data); + + const tonClient = new TonClient({ + endpoint: network.node_url, + }); + + const keyPair = await mnemonicToPrivateKey(mnemonic); + const wallet = WalletContractV4.create({ + publicKey: keyPair.publicKey, + workchain: 0, + }); + + const contract = tonClient.open(wallet); + + const body = beginCell() + .storeUint(0, 32) + .storeStringTail(comment) + .endCell(); + + await contract.sendTransfer({ + seqno: await contract.getSeqno(), + secretKey: keyPair.secretKey, + messages: [ + internal({ + to: to_address, + value: toNano(amount), + body, + bounce: false, + }), + ], + }); +} +``` + + + + The server-side example above shows a native TON transfer. For Jetton transfers, build the Jetton cell as shown in the Jetton section and target the sender's Jetton wallet address. + + +## 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. \ No newline at end of file diff --git a/api-reference/deposit-actions/tron.mdx b/api-reference/deposit-actions/tron.mdx new file mode 100644 index 0000000..81bd6c3 --- /dev/null +++ b/api-reference/deposit-actions/tron.mdx @@ -0,0 +1,159 @@ +--- +title: 'Tron' +description: 'Execute deposit transactions on Tron for TRC-20 token transfers.' +--- + +import DepositoryDepositTron from "/snippets/depository-deposit-tron.mdx"; + +## Overview + +Tron deposits require building a TRC-20 token transfer transaction using TronWeb, then appending the `call_data` as hex-encoded data before signing and broadcasting. + +**Prerequisites:** +- [tronweb](https://github.com/tronprotocol/tronweb) for transaction building and RPC communication + +**Supported networks:** Tron Mainnet, Tron Testnet (Shasta/Nile) + +## call_data Format + +For Tron, `call_data` is a **plain string** (a memo identifier). It is converted to hex and embedded in the transaction data field: + +```javascript +const hexData = Buffer.from(callData).toString("hex"); +``` + +This hex data is appended to the transaction using TronWeb's `addUpdateData` method, which Layerswap uses to identify and match the deposit. + +## Full Example + +The `Server-side` tab signs with a raw private key. The `Browser` tab uses a Tron wallet adapter (TronLink, etc.) and its `signTransaction` method. + + +```typescript Server-side +import { TronWeb } from "tronweb"; + +async function executeTronDeposit( + depositAction: any, + senderAddress: string, + privateKey: string +) { + const { call_data, to_address, amount, token, network } = depositAction; + + const tronWeb = new TronWeb({ + fullNode: network.node_url, + solidityNode: network.node_url, + privateKey, + }); + + const amountInBaseUnits = Math.floor( + amount * Math.pow(10, token.decimals) + ); + + // Build the TRC-20 transfer via smart contract + const { transaction: initialTransaction } = + await tronWeb.transactionBuilder.triggerSmartContract( + token.contract, + "transfer(address,uint256)", + { + feeLimit: 100_000_000, // 100 TRX fee limit + }, + [ + { type: "address", value: to_address }, + { type: "uint256", value: amountInBaseUnits }, + ], + senderAddress + ); + + // Append call_data as hex data + const hexData = Buffer.from(call_data).toString("hex"); + const transaction = await tronWeb.transactionBuilder.addUpdateData( + initialTransaction, + hexData, + "hex" + ); + + // Sign and broadcast + const signedTransaction = await tronWeb.trx.sign(transaction); + const result = await tronWeb.trx.sendRawTransaction(signedTransaction); + + if (result.result) { + return signedTransaction.txID; + } + + throw new Error("Transaction broadcast failed"); +} +``` + +```typescript Browser +import { TronWeb } from "tronweb"; + +async function executeTronDeposit( + depositAction: any, + senderAddress: string, + signTransaction: (tx: any) => Promise +) { + const { call_data, to_address, amount, token, network } = depositAction; + + const tronWeb = new TronWeb({ + fullNode: network.node_url, + solidityNode: network.node_url, + }); + + const amountInBaseUnits = Math.floor( + amount * Math.pow(10, token.decimals) + ); + + const { transaction: initialTransaction } = + await tronWeb.transactionBuilder.triggerSmartContract( + token.contract, + "transfer(address,uint256)", + { feeLimit: 100_000_000 }, + [ + { type: "address", value: to_address }, + { type: "uint256", value: amountInBaseUnits }, + ], + senderAddress + ); + + const hexData = Buffer.from(call_data).toString("hex"); + const transaction = await tronWeb.transactionBuilder.addUpdateData( + initialTransaction, + hexData, + "hex" + ); + + // Sign using the wallet adapter + const signedTransaction = await signTransaction(transaction); + const result = await tronWeb.trx.sendRawTransaction(signedTransaction); + + if (result.result) { + return signedTransaction.txID; + } + + throw new Error("Transaction broadcast failed"); +} +``` + + +## Funding via the Depository + +The example above covers the standard transfer flow. If you created the swap with `use_depository: true`, +the deposit action targets Layerswap's on-chain [Depository](/api-reference/depository) contract. On Tron +the Depository supports **TRC-20 tokens only** — approve the contract, then call `depositERC20`: + + + +See [Choosing a method](/api-reference/deposit-actions/choosing-a-method) for when to use each. + +## 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, or the [Depository](/api-reference/depository) guide to fund via the on-chain contract (`use_depository`, TRC-20 tokens). diff --git a/api-reference/depository.mdx b/api-reference/depository.mdx index 555c4d2..7c12a20 100644 --- a/api-reference/depository.mdx +++ b/api-reference/depository.mdx @@ -1,9 +1,11 @@ --- title: "Depository" description: "Fund a Layerswap swap by calling an on-chain contract instead of sending to a generated deposit address — ideal for smart-contract, server, and programmable wallets." -icon: building-columns --- +import DepositoryDepositEvm from "/snippets/depository-deposit-evm.mdx"; +import DepositoryDepositTron from "/snippets/depository-deposit-tron.mdx"; + ## Overview The **Depository** is a Layerswap-operated smart contract that acts as an on-chain entry point for @@ -23,16 +25,10 @@ arguments, including the swap `id` and `receiver` that Layerswap assigns, are al `encoded_args` if you'd rather build or verify the call yourself. -### Depository vs. deposit address vs. direct transfer - -| Funding method | How you fund | Best for | -| --- | --- | --- | -| **Depository** (`use_depository: true`) | Call a contract method with pre-encoded `call_data` | Aggregators; smart-contract wallets; deterministic target; batching approve + deposit | -| **Deposit address** (`use_deposit_address: true`) | Send funds to a generated address | Exchanges, custodial flows, users sending from anywhere | -| **Direct transfer** (default) | Transfer to the solver's address | Simple EOA wallet transfers | - -The Depository and deposit-address methods are **mutually exclusive** — setting both -`use_depository: true` and `use_deposit_address: true` is rejected. +The Depository is one of three ways to fund a swap — see +[Choosing a method](/api-reference/deposit-actions/choosing-a-method) for how it compares to a deposit +address or a direct transfer, and when to use each. The Depository and deposit-address methods are +**mutually exclusive** — setting both `use_depository: true` and `use_deposit_address: true` is rejected. ## Supported networks @@ -170,27 +166,14 @@ amount. Native deposits skip this step. ### 4. Submit the deposit transaction -```ts -import { createWalletClient, custom, parseAbi } from "viem"; - -const action = swap.deposit_actions[0]; -const wallet = createWalletClient({ transport: custom(window.ethereum) }); - -// ERC20 only: approve the Depository to spend the token -await wallet.writeContract({ - address: action.token.contract, // source_token contract - abi: parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]), - functionName: "approve", - args: [action.to_address, depositAmountInBaseUnits], -}); - -// Submit the prepared deposit call exactly as returned by the API -await wallet.sendTransaction({ - to: action.to_address, - data: action.call_data, - value: BigInt(action.amount_in_base_units), // 0 for ERC20, the deposit amount for native -}); -``` + + + + + + + + The swap `id` and the `receiver` are assigned by Layerswap — use the exact values from the deposit diff --git a/api-reference/refunds.mdx b/api-reference/refunds.mdx index d074eee..06ff748 100644 --- a/api-reference/refunds.mdx +++ b/api-reference/refunds.mdx @@ -1,7 +1,6 @@ --- title: 'Refunds' description: 'Learn how Layerswap handles refunds' -icon: arrow-rotate-left --- When a swap cannot be completed after the user has sent funds, Layerswap automatically initiates a refund. The refund is always processed on the **source chain** in the **source token**. Gas fees for processing the refund transaction are deducted from the refund amount. diff --git a/api-reference/swap-lifecycle.mdx b/api-reference/swap-lifecycle.mdx index a245320..dd00b80 100644 --- a/api-reference/swap-lifecycle.mdx +++ b/api-reference/swap-lifecycle.mdx @@ -1,7 +1,7 @@ --- title: 'Swap lifecycle' +sidebarTitle: 'Overview' description: 'Understand the different statuses a swap goes through from creation to completion or refund.' -icon: arrows-spin ---
diff --git a/docs.json b/docs.json index a4d785e..90cf7d9 100644 --- a/docs.json +++ b/docs.json @@ -131,7 +131,34 @@ "group": "Documentation", "pages": [ "integration/API", - "api-reference/depository", + { + "group": "Deposit Actions", + "icon": "paper-plane", + "pages": [ + "api-reference/deposit-actions/overview", + { + "group": "Deposit methods", + "icon": "money-bill-transfer", + "pages": [ + "api-reference/deposit-actions/choosing-a-method", + "api-reference/depository" + ] + }, + { + "group": "Network support", + "icon": "network-wired", + "pages": [ + "api-reference/deposit-actions/evm", + "api-reference/deposit-actions/bitcoin", + "api-reference/deposit-actions/solana", + "api-reference/deposit-actions/starknet", + "api-reference/deposit-actions/ton", + "api-reference/deposit-actions/tron", + "api-reference/deposit-actions/fuel" + ] + } + ] + }, { "group": "Swap lifecycle", "icon": "arrows-spin", diff --git a/integration/API.mdx b/integration/API.mdx index 47514b3..a920f24 100644 --- a/integration/API.mdx +++ b/integration/API.mdx @@ -1,34 +1,181 @@ --- title: 'API Integration' -description: '' +description: 'Move tokens across chains with the Layerswap API: discover routes, quote, create a swap, execute the deposit, and track it to completion.' icon: server --- ## Overview -Layerswap API is set to enable fast and reliable crypto token swaps across networks - -Layerswap handles millions of dollars in transactions every day across multiple networks. We ensure quick and reliable crypto transfers, giving the freedom to move crypto anywhere. - -## Quickstart - - - - Get supported sources/networks, available routes and swap fees. - - - Create Swap using the Layerswap API. - - - Layerswap will monitor the source network for a transaction and will try to match it with the corresponding swap - - - Once the transaction is matched and added to the swap, Layerswap will initiate a counterparty transaction to the destination_address - - - Poll via Get Swap endpoint to see if the matching transaction Input was added to the swap - - - If the webhook is configured, Layerswap will deliver a swap status update notification to the partner-specified URL - - \ No newline at end of file +The Layerswap API moves tokens across chains programmatically. The flow is the same for every route: +**discover** a route, request a **quote**, **create** a swap, submit one on-chain **deposit** on the +source chain, and Layerswap **delivers** the funds on the destination chain — then you **track** the swap +to completion. + +- **Base URL:** `https://api.layerswap.io` +- **API key (optional):** include your key in the `X-LS-APIKEY` header to attribute swaps to your + account (for analytics and reporting). Create one on the [API Keys](/api-keys) page. +- **Endpoints:** every endpoint, with request/response schemas, is in the **Endpoints** section of this + tab — and is linked inline throughout this guide. + +## Core concepts + +| Term | Meaning | +| --- | --- | +| **Network** | A blockchain, identified by name (`ETHEREUM_MAINNET`, `ARBITRUM_MAINNET`, …). EVM networks also accept their numeric `chain_id`. | +| **Token** | An asset on a network, identified by **symbol** or **contract address**. Prefer the contract address — see the warning in step 1. | +| **Route** | A supported `(source network, source token) → (destination network, destination token)` pair. | +| **Quote** | The expected output amount, fees, and duration for a given input amount. | +| **Swap** | A single cross-chain transfer you create. It moves through a [lifecycle](/api-reference/swap-lifecycle) from creation to completion. | +| **Deposit action** | The exact on-chain transaction to submit on the source chain to fund a swap. Returned with the swap — see [Deposit Actions](/api-reference/deposit-actions/overview). | +| **Depository** | An optional Layerswap contract you can fund through instead of a plain transfer — ideal for smart-contract, server, and programmable wallets. See [Depository](/api-reference/depository). | + +## 1. Discover routes & tokens + +Find which networks and tokens you can bridge between, and the amount limits for a route. + +| Endpoint | Returns | +| --- | --- | +| [`GET /api/v2/sources`](/api-reference/swaps/get-sources) | Source networks and their tokens (optionally filtered by `destination_network` / `destination_token`) | +| [`GET /api/v2/destinations`](/api-reference/swaps/get-destinations) | Destination networks and their tokens (optionally filtered by `source_network` / `source_token`) | +| [`GET /api/v2/limits`](/api-reference/swaps/get-swap-route-limits) | `min_amount` / `max_amount` (and their USD values) for a specific route | + +Each token in the `/sources` and `/destinations` response includes `symbol`, `contract`, `decimals`, +`price_in_usd`, `status`, and `group` — the **token family** (e.g. `ETH`, `USDC`, `USDT`) that ties a +token's variants together across networks. + + + **Identify tokens by contract address, not by a hard-coded symbol.** A token's `symbol` is a display + label and can change — Arbitrum's USDT was renamed to **USDT0**, so a hard-coded + `"source_token": "USDT"` silently stops matching. The contract address is stable. Either pass the + **contract address** as `source_token` / `destination_token`, or fetch the live token list from + `/sources` and resolve by the `group` field. **Don't hard-code symbols, contract addresses, or + decimals — discover them.** + + +```js +const sources = await fetch("https://api.layerswap.io/api/v2/sources", { + headers: { "X-LS-APIKEY": process.env.LAYERSWAP_API_KEY }, +}).then((r) => r.json()); + +// Resolve the USDT-family token on Arbitrum by its `group`, not its symbol — +// this stays correct even though Arbitrum's USDT is now displayed as USDT0. +const arbitrum = sources.data.find((n) => n.name === "ARBITRUM_MAINNET"); +const usdt = arbitrum.tokens.find((t) => t.group === "USDT"); + +usdt.symbol; // "USDT0" → display label, may change +usdt.contract; // "0x..." → stable identifier, use this as source_token +``` + +The full catalog is also browsable at [Networks & Tokens](/networks-tokens). + +## 2. Get a quote + +[`GET /api/v2/quote`](/api-reference/swaps/get-quote) returns the expected output, fees, and duration for +a given `amount`, without creating a swap — provide the route plus an `amount`. Example response: + +```json +{ + "data": { + "quote": { + "receive_amount": 99.5, + "min_receive_amount": 98.5, + "total_fee": 0.5, + "total_fee_in_usd": 0.5, + "blockchain_fee": 0.15, + "service_fee": 0.35, + "avg_completion_time": "00:02:00", + "rate": 0.995, + "slippage": 0.5 + } + } +} +``` + +See [Fees](/fees) for how fees are composed. + +## 3. Create the swap + +[`POST /api/v2/swaps`](/api-reference/swaps/create-swap) creates the swap and returns it together with the +`deposit_actions` you'll execute next. Choose **one** funding method: + +| Funding method | Set | Best for | +| --- | --- | --- | +| **Direct transfer** (default) | neither flag | Simple EOA wallets transferring to the solver | +| **Deposit address** | `use_deposit_address: true` | Exchanges, custodial flows, users sending from anywhere | +| **Depository contract** | `use_depository: true` | Aggregators; smart-contract, server, and programmable wallets — see [Depository](/api-reference/depository) | + +`use_depository` and `use_deposit_address` are mutually exclusive. + +```bash +curl -X POST https://api.layerswap.io/api/v2/swaps \ + -H "X-LS-APIKEY: $LAYERSWAP_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "source_network": "ARBITRUM_MAINNET", + "source_token": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + "destination_network": "BASE_MAINNET", + "destination_token": "USDC", + "destination_address": "0xRecipient", + "amount": 100, + "use_depository": true + }' +``` + +The response contains the created `swap` (with its `id` and `status`), the `quote`, and the +`deposit_actions` array. + +## 4. Execute the deposit action + +A **deposit action** is the exact on-chain transaction to submit on the source chain. Read it from the +swap response (or re-fetch via +[`GET /api/v2/swaps/{swapId}/deposit_actions`](/api-reference/swaps/get-deposit-actions)): + +```js +const action = swap.deposit_actions[0]; +// action.to_address, action.amount_in_base_units, action.call_data, action.network, action.token +``` + +How you submit it depends on the source chain — the `call_data` format differs per network (EVM calldata, +a Bitcoin `OP_RETURN` memo, a Solana transaction, etc.). Follow the chain-specific guide: + +- **[Deposit Actions](/api-reference/deposit-actions/overview)** — per-chain execution (EVM, Bitcoin, + Solana, Starknet, TON, Tron, Fuel). +- **[Depository](/api-reference/depository)** — when you created the swap with `use_depository: true`. + + + Submit the returned `call_data` **verbatim**. It encodes the swap `id` and the assigned receiver; + altering it means Layerswap can't match your on-chain deposit to the swap. + + +After broadcasting, you can optionally speed up matching by reporting the transaction hash to +[`POST /api/v2/swaps/{swapId}/deposit_speedup`](/api-reference/swaps/speed-up-deposit). + +## 5. Track the transfer + +Poll the swap to follow it to completion: + +- [`GET /api/v2/swaps/{swapId}`](/api-reference/swaps/get-swap-details) — the swap and its `transactions` + (`input` = your source deposit, `output` = the destination delivery). +- [`GET /api/v2/swaps/by_transaction_hash/{transactionHash}`](/api-reference/swaps/get-swap-by-transaction-hash) + — look up a swap by its source transaction hash. + +A typical swap moves `user_transfer_pending` → `ls_transfer_pending` → `completed`. Other terminal states +are `failed`, `expired`, `pending_refund`, and `refunded`. See the [Swap Lifecycle](/api-reference/swap-lifecycle) +for every state and transition, and [Refunds](/api-reference/refunds) for failure handling. + +## Next steps + + + + Execute the deposit on each supported chain. + + + Fund a swap by calling our on-chain contract. + + + Every swap state and how to react to it. + + + How quotes and fees are calculated. + + diff --git a/snippets/depository-deposit-evm.mdx b/snippets/depository-deposit-evm.mdx new file mode 100644 index 0000000..35195f4 --- /dev/null +++ b/snippets/depository-deposit-evm.mdx @@ -0,0 +1,25 @@ +```ts viem +import { createWalletClient, custom, parseAbi } from "viem"; + +// `swap` is the POST /api/v2/swaps response +const action = swap.deposit_actions[0]; +const wallet = createWalletClient({ transport: custom(window.ethereum) }); + +// ERC-20 only: approve the Depository (to_address) to pull the tokens. +// The amount is the 4th depositERC20 argument, returned in encoded_args. +if (action.token.contract) { + await wallet.writeContract({ + address: action.token.contract, + abi: parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]), + functionName: "approve", + args: [action.to_address, BigInt(action.encoded_args[3])], + }); +} + +// Submit the prepared depositNative / depositERC20 call exactly as returned +await wallet.sendTransaction({ + to: action.to_address, // the Depository contract + data: action.call_data, + value: BigInt(action.amount_in_base_units), // 0 for ERC-20, the deposit amount for native +}); +``` diff --git a/snippets/depository-deposit-tron.mdx b/snippets/depository-deposit-tron.mdx new file mode 100644 index 0000000..fc583b3 --- /dev/null +++ b/snippets/depository-deposit-tron.mdx @@ -0,0 +1,51 @@ +```ts TronWeb +import { TronWeb } from "tronweb"; + +// `swap` is the POST /api/v2/swaps response; PRIVATE_KEY signs the deposit +const action = swap.deposit_actions[0]; +const [id, , receiverHex, amountHex] = action.encoded_args; // [id, token, receiver, amount] +const amount = BigInt(amountHex).toString(); + +const tronWeb = new TronWeb({ + fullNode: action.network.node_url, + solidityNode: action.network.node_url, + privateKey: PRIVATE_KEY, +}); +const sender = tronWeb.defaultAddress.base58; + +// encoded_args carry EVM-style hex addresses — convert the receiver to a Tron address +const receiver = tronWeb.address.fromHex("41" + receiverHex.replace(/^0x/, "")); + +// 1) Approve the Depository (to_address) to spend the TRC-20 amount +const approveTx = ( + await tronWeb.transactionBuilder.triggerSmartContract( + action.token.contract, // TRC-20 token (base58) + "approve(address,uint256)", + { feeLimit: 100_000_000 }, + [ + { type: "address", value: action.to_address }, + { type: "uint256", value: amount }, + ], + sender, + ) +).transaction; +await tronWeb.trx.sendRawTransaction(await tronWeb.trx.sign(approveTx)); + +// 2) Call depositERC20(bytes32 id, address token, address receiver, uint256 amount) +// on the Depository (wait for the approval to confirm first) +const depositTx = ( + await tronWeb.transactionBuilder.triggerSmartContract( + action.to_address, // Depository contract (base58) + "depositERC20(bytes32,address,address,uint256)", + { feeLimit: 100_000_000 }, + [ + { type: "bytes32", value: id }, + { type: "address", value: action.token.contract }, + { type: "address", value: receiver }, + { type: "uint256", value: amount }, + ], + sender, + ) +).transaction; +await tronWeb.trx.sendRawTransaction(await tronWeb.trx.sign(depositTx)); +```