diff --git a/api-reference/deposit-actions/bitcoin.mdx b/api-reference/deposit-actions/bitcoin.mdx new file mode 100644 index 0000000..94c4369 --- /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 transfer 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/evm.mdx b/api-reference/deposit-actions/evm.mdx new file mode 100644 index 0000000..467a096 --- /dev/null +++ b/api-reference/deposit-actions/evm.mdx @@ -0,0 +1,174 @@ +--- +title: 'EVM Chains' +description: 'Execute deposit transactions on Ethereum, Arbitrum, Optimism, Base, Polygon, and other EVM-compatible networks.' +--- + +## 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 transfer action. + + + Construct a transaction object with the transfer 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. + + +## 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/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..d5efb23 --- /dev/null +++ b/api-reference/deposit-actions/overview.mdx @@ -0,0 +1,118 @@ +--- +title: 'Handling Transfer Actions' +description: 'Learn how to execute transfer transactions on different blockchains after creating a swap via the Layerswap API.' +--- + +## Overview + +When you create a swap via `POST /api/v2/swaps`, the response includes a `deposit_actions` array. Each transfer action contains the destination address, amount, and a blockchain-specific `call_data` payload — everything you need to construct and submit a transaction on the source chain. After broadcasting the transaction, you notify Layerswap with the transaction hash via the speedup endpoint so it can match the deposit and complete the swap. + +## Transfer Action Schema + +Each item in the `deposit_actions` array has the following structure: + + + Either `transfer` (wallet transaction) or `manual_transfer` (manual deposit to an address). Use the action with `type: "transfer"` for programmatic transfers. + + + + The deposit address to send funds to. + + + + The amount to transfer in human-readable units (e.g. `0.1` ETH, not wei). + + + + The amount in the token's smallest unit (e.g. wei for ETH, satoshi for BTC). + + + + Blockchain-specific payload used to construct the transaction. The format varies by chain — see the chain-specific guides below. + + + + The source network object, including `name`, `type`, `chain_id`, and `node_url`. + + + + The token being transferred, including `symbol`, `decimals`, and `contract` (null for native tokens). + + + + The token used to pay transaction fees on this network. + + + + Execution order if there are multiple transfer actions. + + +## Step-by-Step Flow + + + + Call `POST /api/v2/swaps` with `use_deposit_address: false` and include the `source_address` parameter. The response will contain `deposit_actions`. + + + Starknet, Paradex, and TON only support the transfer flow — `use_deposit_address: true` is rejected for these networks. All other source networks accept either mode. + + + ```bash + curl -X POST https://api.layerswap.io/api/v2/swaps \ + -H "X-LS-APIKEY: your_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 + }' + ``` + + + From the response, find the transfer action with `type: "transfer"`: + + ```javascript + const response = await createSwap(/* ... */); + const depositAction = response.deposit_actions.find( + action => action.type === "transfer" + ); + + const { call_data, to_address, amount, network, token } = depositAction; + ``` + + + Use the `call_data`, `to_address`, and `amount` to construct and send a blockchain-specific transaction. See the chain guides below for implementation details. + + + After the transaction is submitted, call the speedup endpoint so Layerswap can match it 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": "0xYourTransactionHash..." }' + ``` + + + Poll `GET /api/v2/swaps/{swap_id}` or use [webhooks](/api-reference/webhook) to track the swap through its [lifecycle](/api-reference/swap-lifecycle). + + + +## call_data Format by Chain + +The `call_data` field is the key piece — its format depends on the source blockchain: + +| Blockchain | call_data Format | Guide | +|---|---|---| +| EVM (Ethereum, Arbitrum, Base, etc.) | Hex-encoded transaction data (`0x...`) | [EVM Guide](/api-reference/deposit-actions/evm) | +| Bitcoin | Numeric memo (encoded as OP_RETURN) | [Bitcoin Guide](/api-reference/deposit-actions/bitcoin) | +| Solana | Base64-encoded serialized transaction | [Solana Guide](/api-reference/deposit-actions/solana) | +| Starknet | JSON array of Starknet calls | [Starknet Guide](/api-reference/deposit-actions/starknet) | +| TON | JSON object with `comment` (and `amount` for Jettons) | [TON Guide](/api-reference/deposit-actions/ton) | +| Tron | String memo (hex-encoded into transaction data) | [Tron Guide](/api-reference/deposit-actions/tron) | +| Fuel | JSON object with `script` and `quantities` | [Fuel Guide](/api-reference/deposit-actions/fuel) | \ No newline at end of file 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..f02f191 --- /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 transfer action's `amount` field. +- `asset` — The source asset symbol (e.g. `TON`, `USDT`). Informational; the actual contract is on the transfer 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..aa336c6 --- /dev/null +++ b/api-reference/deposit-actions/tron.mdx @@ -0,0 +1,147 @@ +--- +title: 'Tron' +description: 'Execute deposit transactions on Tron for TRC-20 token transfers.' +--- + +## 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"); +} +``` + + +## 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. diff --git a/docs.json b/docs.json index c4b3807..a5d844a 100644 --- a/docs.json +++ b/docs.json @@ -147,6 +147,24 @@ "recipes/privy-wallets" ] }, + { + "group": "Transfer Actions", + "pages": [ + "api-reference/deposit-actions/overview", + { + "group": "Blockchains", + "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": "Endpoints", "openapi": { diff --git a/integration/API.mdx b/integration/API.mdx index 47514b3..ecf7b8e 100644 --- a/integration/API.mdx +++ b/integration/API.mdx @@ -19,11 +19,8 @@ Layerswap handles millions of dollars in transactions every day across multiple 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 + + Use the `deposit_actions` from the swap response to submit a transaction on the source chain. See [Handling Deposits](/api-reference/deposit-actions/overview) for chain-specific guides. Poll via Get Swap endpoint to see if the matching transaction Input was added to the swap