Skip to main content
CROSS-CHAIN

Cross-Chain Development

The Arbiter bridge moves ELA between the main chain and sidechains (ESC, EID) at a 1:1 ratio. For architecture details — SPV proofs, arbiter quorum, trust model — see the Bridge System overview.

Deposit: Main Chain → ESC

To send ELA from the main chain to ESC, create a TransferCrossChainAsset transaction (type 0x08):

import {
Transaction,
TransactionInput,
TransactionOutput,
CrossChainOutputPayload,
} from "@elastosfoundation/wallet-js-sdk";

async function depositToESC(
fromPrivKey: Buffer,
fromPubKey: Buffer,
fromAddress: string,
escAddress: string, // 0x-prefixed ESC address
amountSela: bigint
): Promise<string> {
const utxos = await getUTXOs(fromAddress);
const crossChainFee = 10000n; // 0.0001 ELA cross-chain fee per output
const txFee = 10000n;
const totalNeeded = amountSela + crossChainFee + txFee;

let totalInput = 0n;
const selectedUTXOs: UTXO[] = [];
for (const utxo of utxos) {
selectedUTXOs.push(utxo);
totalInput += BigInt(Math.round(parseFloat(utxo.amount) * 1e8));
if (totalInput >= totalNeeded) break;
}

if (totalInput < totalNeeded) {
throw new Error("Insufficient funds");
}

const tx = new Transaction();
tx.txType = 0x08; // TransferCrossChainAsset

// Add inputs
for (const utxo of selectedUTXOs) {
const input = new TransactionInput();
input.txid = utxo.txid;
input.vout = utxo.vout;
input.sequence = 0xfffffffe;
tx.addInput(input);
}

// Cross-chain output — sent to the cross-chain burn address
const crossChainOutput = new TransactionOutput();
crossChainOutput.address = Transaction.CROSS_CHAIN_ADDRESS; // burn address
crossChainOutput.amount = amountSela + crossChainFee;
crossChainOutput.assetId = Transaction.ELA_ASSET_ID;

// Cross-chain payload specifies the destination ESC address
const payload = new CrossChainOutputPayload();
payload.targetAddress = escAddress;
payload.targetAmount = amountSela;
payload.targetData = Buffer.alloc(0);
crossChainOutput.payload = payload;
tx.addOutput(crossChainOutput);

// Change output
const changeSela = totalInput - amountSela - crossChainFee - txFee;
if (changeSela > 0n) {
const changeOutput = new TransactionOutput();
changeOutput.address = fromAddress;
changeOutput.amount = changeSela;
changeOutput.assetId = Transaction.ELA_ASSET_ID;
tx.addOutput(changeOutput);
}

// Sign and broadcast
for (let i = 0; i < tx.inputs.length; i++) {
tx.signInput(i, fromPrivKey, fromPubKey, SignType.SIGN_TYPE_P256);
}

const rawTx = tx.serialize().toString("hex");
return await broadcastTransaction(rawTx);
}

What happens after broadcast:

  1. Main chain miners include the transaction in a block
  2. The Arbiter bridge detects the cross-chain transaction
  3. Arbiters generate an SPV proof of the main chain transaction
  4. The SPV proof is submitted to the ESC chain
  5. ESC auto-credits the ELA to the specified 0x address
  6. Typical confirmation time: 6-12 minutes
info

For the getUTXOs and broadcastTransaction helper functions, see ELA Main Chain SDK.

Withdraw: ESC → Main Chain

To withdraw ELA from ESC back to the main chain, call the ESC withdrawal contract:

import { ethers } from "ethers";

const ESC_WITHDRAW_CONTRACT = "0x0000000000000000000000000000000000000064"; // precompiled address

async function withdrawToMainChain(
signer: ethers.Signer,
elaMainChainAddress: string, // Base58Check ELA main chain address
amountELA: string // amount in ELA (e.g., "1.5")
): Promise<string> {
const crossChainFee = ethers.parseEther("0.0001"); // 10000 sela
const amount = ethers.parseEther(amountELA);
const totalValue = amount + crossChainFee;

// The withdrawal contract accepts ELA and burns it
// The main chain address is encoded in the call data
const iface = new ethers.Interface([
"function withdrawELA(string mainchainAddress) payable",
]);

const tx = await signer.sendTransaction({
to: ESC_WITHDRAW_CONTRACT,
value: totalValue,
data: iface.encodeFunctionData("withdrawELA", [elaMainChainAddress]),
});

const receipt = await tx.wait();
console.log("Withdrawal tx:", receipt!.hash);
return receipt!.hash;
}

// What happens next:
// 1. ELA is burned on ESC
// 2. Arbiters detect the burn event
// 3. Arbiters create a multi-sig withdrawal transaction on the main chain
// 4. Once enough arbiters sign to meet the multi-sig threshold, the main chain tx is broadcast
// 5. ELA appears at the specified main chain address
// Typical time: 10-20 minutes

Cross-Chain Status Tracking

// Check if a deposit has been credited on ESC
async function checkDepositStatus(mainChainTxId: string): Promise<string> {
// Query the ESC chain for the cross-chain transaction
const provider = new ethers.JsonRpcProvider("https://api.elastos.io/esc");

// Cross-chain deposits appear as internal transactions
// Check the recipient's balance or look for the cross-chain event
const response = await fetch(
`https://esc.elastos.io/api?module=transaction&action=gettxinfo&txhash=${mainChainTxId}`
);
const data = await response.json();
return data.result?.status || "pending";
}

Cross-Chain Fees

DirectionFee
Main → ESC (deposit)0.0001 ELA (10,000 sela) per cross-chain output
ESC → Main (withdrawal)0.0001 ELA (10,000 sela)
Main → EID (deposit)0.0001 ELA (10,000 sela) per cross-chain output
EID → Main (withdrawal)0.0001 ELA (10,000 sela)
warning

The cross-chain output amount must include the cross-chain fee on top of the transfer amount. If you omit it, the transaction will be rejected. See Common Pitfalls for details.

The exchange rate is always exactly 1:1, hardcoded in the bridge code. There is no slippage or variable pricing.