Skip to main content

Overview

This guide will walk you through executing a complete cross-chain bridge transaction using LiFi Contract Types with the Across Protocol integration.

Prerequisites

Before starting, ensure you have:
  • Node.js 16+ installed
  • A basic understanding of ethers.js
  • Access to an Ethereum RPC provider (Infura, Alchemy, etc.)
  • A wallet with some ETH for gas fees

Installation

First, install the required packages:
npm install lifi-contract-typings ethers

Complete Example

Here’s a complete working example that bridges USDC from Ethereum to Polygon using Across Protocol:
import { ethers } from 'ethers';
import { 
  AcrossFacet__factory, 
  ILiFi,
  AcrossFacet 
} from 'lifi-contract-typings';

// Configuration
const LIFI_DIAMOND_ADDRESS = '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE';
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // Ethereum USDC
const RPC_URL = 'https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY';

async function bridgeUSDCToPolygon() {
  // Setup provider and signer
  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
  const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);
  
  // Connect to LiFi Diamond contract with AcrossFacet interface
  const acrossFacet = AcrossFacet__factory.connect(
    LIFI_DIAMOND_ADDRESS,
    wallet
  );
  
  // Prepare bridge data
  const bridgeData: ILiFi.BridgeDataStruct = {
    transactionId: ethers.utils.formatBytes32String(
      `lifi-tx-${Date.now()}`
    ),
    bridge: 'across',
    integrator: 'my-dapp',
    referrer: ethers.constants.AddressZero,
    sendingAssetId: USDC_ADDRESS,
    receiver: wallet.address, // Receive on same address
    minAmount: ethers.utils.parseUnits('100', 6), // 100 USDC (6 decimals)
    destinationChainId: 137, // Polygon
    hasSourceSwaps: false,
    hasDestinationCall: false,
  };
  
  // Prepare Across-specific data
  const acrossData: AcrossFacet.AcrossDataStruct = {
    relayerFeePct: ethers.BigNumber.from('10000000000000000'), // 1% fee
    quoteTimestamp: Math.floor(Date.now() / 1000),
    message: '0x',
    maxCount: ethers.constants.MaxUint256,
  };
  
  // Approve USDC spending (if not already approved)
  const usdcContract = new ethers.Contract(
    USDC_ADDRESS,
    ['function approve(address spender, uint256 amount) returns (bool)'],
    wallet
  );
  
  console.log('Approving USDC...');
  const approveTx = await usdcContract.approve(
    LIFI_DIAMOND_ADDRESS,
    bridgeData.minAmount
  );
  await approveTx.wait();
  console.log('USDC approved!');
  
  // Execute the bridge transaction
  console.log('Starting bridge transaction...');
  const tx = await acrossFacet.startBridgeTokensViaAcross(
    bridgeData,
    acrossData
  );
  
  console.log('Transaction submitted:', tx.hash);
  
  // Wait for confirmation
  const receipt = await tx.wait();
  console.log('Bridge transaction confirmed!');
  console.log('Transaction receipt:', receipt.transactionHash);
  
  return receipt;
}

// Execute
bridgeUSDCToPolygon()
  .then(() => console.log('Bridge completed successfully!'))
  .catch(console.error);

Step-by-Step Breakdown

Let’s break down the key parts:
1

Connect to the Contract

Use the factory to create a type-safe contract instance:
const acrossFacet = AcrossFacet__factory.connect(
  LIFI_DIAMOND_ADDRESS,
  wallet
);
This gives you a fully typed contract with autocomplete for all methods.
2

Prepare Bridge Data

Create the BridgeDataStruct with all required parameters:
const bridgeData: ILiFi.BridgeDataStruct = {
  transactionId: ethers.utils.formatBytes32String("unique-id"),
  bridge: "across",
  integrator: "my-dapp",
  referrer: ethers.constants.AddressZero,
  sendingAssetId: USDC_ADDRESS,
  receiver: wallet.address,
  minAmount: ethers.utils.parseUnits("100", 6),
  destinationChainId: 137,
  hasSourceSwaps: false,
  hasDestinationCall: false,
};
The transactionId should be unique for each transaction. Use it to track your bridges.
3

Prepare Protocol-Specific Data

Each bridge protocol requires specific parameters. For Across:
const acrossData: AcrossFacet.AcrossDataStruct = {
  relayerFeePct: ethers.BigNumber.from("10000000000000000"), // 1%
  quoteTimestamp: Math.floor(Date.now() / 1000),
  message: "0x",
  maxCount: ethers.constants.MaxUint256,
};
4

Approve Token Spending

Before bridging, approve the LiFi Diamond to spend your tokens:
await usdcContract.approve(
  LIFI_DIAMOND_ADDRESS,
  bridgeData.minAmount
);
5

Execute Bridge Transaction

Call the bridge method with your prepared data:
const tx = await acrossFacet.startBridgeTokensViaAcross(
  bridgeData,
  acrossData
);
await tx.wait();

Listening to Events

Monitor your bridge transaction with type-safe event listeners:
import { ILiFi } from 'lifi-contract-typings';

// Listen for transfer started events
acrossFacet.on(
  'LiFiTransferStarted',
  (bridgeData: ILiFi.BridgeDataStructOutput) => {
    console.log('Transfer started!');
    console.log('Transaction ID:', bridgeData.transactionId);
    console.log('Destination Chain:', bridgeData.destinationChainId.toString());
    console.log('Amount:', bridgeData.minAmount.toString());
  }
);

// Listen for transfer completed events
acrossFacet.on(
  'LiFiTransferCompleted',
  (transactionId, receivingAssetId, receiver, amount, timestamp) => {
    console.log('Transfer completed!');
    console.log('Amount received:', amount.toString());
  }
);

Bridging with Source Swaps

You can swap tokens before bridging using the LibSwap.SwapDataStruct:
import { LibSwap } from 'lifi-contract-typings';

const swapData: LibSwap.SwapDataStruct[] = [
  {
    callTo: '0x...', // DEX router address
    approveTo: '0x...', // Address to approve
    sendingAssetId: '0x...', // ETH address
    receivingAssetId: USDC_ADDRESS,
    fromAmount: ethers.utils.parseEther('1'),
    callData: '0x...', // Encoded swap call
    requiresDeposit: true,
  },
];

// Update bridge data
bridgeData.hasSourceSwaps = true;

// Call the swap+bridge method
const tx = await acrossFacet.swapAndStartBridgeTokensViaAcross(
  bridgeData,
  swapData,
  acrossData
);

Common Bridge Integrations

Here are the factory imports for other popular bridges:
import { StargateFacetV2__factory } from 'lifi-contract-typings';

const stargateFacet = StargateFacetV2__factory.connect(
  LIFI_DIAMOND_ADDRESS,
  signer
);

Error Handling

Always wrap your calls in try-catch blocks:
try {
  const tx = await acrossFacet.startBridgeTokensViaAcross(
    bridgeData,
    acrossData
  );
  await tx.wait();
} catch (error) {
  if (error.code === 'INSUFFICIENT_FUNDS') {
    console.error('Not enough ETH for gas');
  } else if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
    console.error('Transaction would fail - check approvals and balances');
  } else {
    console.error('Bridge failed:', error.message);
  }
}

Testing

For testing, use a local fork or testnet:
import { ethers } from 'hardhat';

// Use Hardhat's network fork
const provider = ethers.provider;
const [signer] = await ethers.getSigners();

const acrossFacet = AcrossFacet__factory.connect(
  LIFI_DIAMOND_ADDRESS,
  signer
);
Never commit private keys to version control. Use environment variables:
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

Next Steps

Core Concepts

Learn about BridgeData, SwapData, and other key types

API Reference

Explore all available contracts and methods

Examples

Browse more examples for different use cases

LiFi SDK

Use the higher-level LiFi SDK for easier integration

Resources

Build docs developers (and LLMs) love