Skip to main content
Portal’s Web SDK provides comprehensive cross-chain bridging and swapping capabilities through the portal.trading.lifi API. This guide covers getting quotes, finding routes, executing swaps and bridges, and tracking transaction status.

Overview

The Li.Fi functionality allows you to:
  • Get quotes for bridging or swapping tokens across chains
  • Find routes to discover the best paths for your cross-chain transfers
  • Execute swaps and bridges by signing and submitting transactions
  • Track transaction status for cross-chain transfers

Prerequisites

Before using Li.Fi operations, ensure you have:
  • A properly initialized Portal client
  • An active wallet with the required token(s) on the source network (see Create a wallet)
  • Li.Fi integration enabled in your Portal Dashboard (see Li.Fi Integration)

Getting a Quote

Use the getQuote method to get a quote for bridging or swapping tokens across chains.
const address = await portal.getEip155Address();
if (!address) return;

const request = {
  fromChain: 'eip155:8453', // Base Mainnet
  toChain: 'eip155:42161', // Arbitrum
  fromToken: 'ETH',
  toToken: 'USDC',
  fromAddress: address,
  fromAmount: '100000000000000', // 0.0001 ETH (in wei)
};

const response = await portal.trading.lifi.getQuote(request);
const rawResponse = response.data?.rawResponse;

if (rawResponse) {
  // Process quote response
  // You can sign and submit the transaction if transactionRequest is available
  if (rawResponse.transactionRequest) {
    await executeTransaction(rawResponse.transactionRequest, request.fromChain);
  }
}
The response includes a transactionRequest object with the transaction details you’ll need to sign and submit.

Finding Routes

Use the getRoutes method to discover available routes for your cross-chain transfer.
const address = await portal.getEip155Address();
if (!address) return;

const request = {
  fromChainId: 'eip155:8453', // Base Mainnet
  fromAmount: '100000000000000', // 0.0001 ETH (in wei)
  fromTokenAddress: 'ETH',
  toChainId: 'eip155:42161', // Arbitrum
  toTokenAddress: 'USDC',
  fromAddress: address,
};

const response = await portal.trading.lifi.getRoutes(request);
const rawResponse = response.data?.rawResponse;

if (rawResponse) {
  const routes = rawResponse.routes;

  // Find recommended route
  const recommendedRoute = routes.find(route => route.tags?.includes('RECOMMENDED')) || routes[0];

  if (recommendedRoute) {
    console.log('Selected route:', recommendedRoute.id);
    console.log('Steps:', recommendedRoute.steps.length);
    console.log('From:', recommendedRoute.fromAmountUSD, 'USD');
    console.log('To:', recommendedRoute.toAmountUSD, 'USD');

    // Process route steps
    await processRouteSteps(recommendedRoute.steps, request.fromChainId);
  }
}
The response includes an array of routes with estimates, fees, and gas costs. Routes may be tagged as RECOMMENDED, CHEAPEST, or FASTEST.

Getting Route Step Details

Use the getRouteStep method to get detailed transaction information for a specific route step, including an unsigned transaction that you can then sign and submit to an RPC provider (the transactionRequest field).
async function getStepTransactionDetails(step: LifiStep): Promise<LifiStep | null> {
  try {
    const response = await portal.trading.lifi.getRouteStep(step);
    return response.data?.rawResponse || null;
  } catch (error) {
    console.error('Error getting step details:', error.message);
    return null;
  }
}
The response includes a transactionRequest object with the unsigned transaction that you can sign and submit.

Executing Swaps and Bridges

After getting a quote or route step details, extract the transaction details from the transactionRequest object and sign the transaction. Extract the from, to, value, and data fields to sign and submit the transaction.

Signing and Submitting Transactions

async function executeTransaction(transactionRequest: LifiTransactionRequest, chainId: string) {
  try {
    const from = transactionRequest.from;
    const to = transactionRequest.to;

    if (!from || !to) {
      console.error("Missing required 'from' or 'to' field");
      return;
    }

    // Extract value. Default: 0x0
    const value = transactionRequest.value || '0x0';

    // Extract data
    const data = transactionRequest.data || '';

    // Create transaction
    const ethTransaction = {
      from,
      to,
      value,
      data,
      // Let Portal handle gas estimation
    };

    // Sign and send
    const sendResponse = await portal.request(chainId, 'eth_sendTransaction', [ethTransaction]);

    const txHash = sendResponse.result as string;
    if (txHash) {
      console.log('Transaction submitted:', txHash);

      // Wait for on-chain confirmation
      const confirmed = await waitForConfirmation(txHash, chainId);
      if (confirmed) {
        console.log('Transaction confirmed');
      }
    }
  } catch (error) {
    console.error('Error executing transaction:', error.message);
  }
}
The transactionRequest from Li.Fi may include gasPrice and gasLimit fields. You can remove these if you want Portal to estimate the gas for you, or include them if you want to use Li.Fi’s estimates.

Processing Multiple Route Steps

For routes with multiple steps, process them sequentially:
async function processRouteSteps(steps: LifiStep[], fromChainId: string): Promise<boolean> {
  for (let index = 0; index < steps.length; index++) {
    const step = steps[index];
    console.log(`Processing step ${index + 1}/${steps.length}: ${step.tool}`);

    // 1. Get transaction details for this step
    const stepWithTx = await getStepTransactionDetails(step);
    const transactionRequest = stepWithTx?.transactionRequest;

    if (!transactionRequest) {
      console.error(`Failed to get transaction details for step ${index + 1}`);
      return false;
    }

    // 2. Sign and submit the transaction
    await executeTransaction(transactionRequest, fromChainId);

    console.log(`Step ${index + 1} completed`);
  }

  return true;
}

Waiting for Transaction Confirmation

async function waitForConfirmation(
  txHash: string,
  chainId: string,
  desiredConfirmations: number = 2,
  pollIntervalMs: number = 2000,
  timeoutSeconds: number = 600
): Promise<boolean> {
  const startTime = Date.now();
  let minedBlock = null; // will be set once mined
  let confirmations = 0;

  while (true) {
    if (timeoutSeconds && (Date.now() - startTime) / 1000 > timeoutSeconds) {
      throw new Error(`Timeout waiting for ${desiredConfirmations} confirmations`);
    }

    try {
      if (!minedBlock) {
        const receiptRes = await axios.post(
          getRpcUrl(chainId), // Create a method to obtain the RPC URL for the network of the Li.Fi transaction step
          {
            jsonrpc: '2.0',
            id: 1,
            method: getRpcReceiptMethod(chainId), // i.e. 'eth_getTransactionReceipt' for non-abstracted eth accounts
            params: [txHash],
          }
        );

        const receipt = receiptRes.data.result; // If the account is abstracted, it should be receiptRes?.data?.result?.receipt.

        if (!receipt) {
          await new Promise(r => setTimeout(r, pollIntervalMs));
          continue;
        }

        // For Ethereum-based chains, check if the transaction failed by checking: receipt.status === '0x0'
        if (checkIfStatusFailed(receipt, chainId)) {
          throw new Error(`Transaction ${txHash} reverted`);
        }

        minedBlock = parseInt(receipt.blockNumber, 16);
        // fall through to Phase 2 on the same loop iteration if possible
      }

      // Phase 2: already mined → now we only need latest block number
      const latestBlockRes = await axios.post(
        getRpcUrl(chainId), // Create a method to obtain the RPC URL for the network of the Li.Fi step transaction
        {
          jsonrpc: '2.0',
          id: 2,
          method: getRpcLatestBlockMethod(chainId),
          params: [],
        }
      );

      /*
       * If on eth, then the getConfirmations function would do this:
       *   const latestBlock = parseInt(latestBlockRes.data.result, 16)
       *   return latestBlock - minedBlock + 1
       */
      confirmations = getConfirmations(latestBlockRes, minedBlock);

      if (confirmations >= desiredConfirmations) {
        return true;
      }
    } catch (err) {
      console.error('Polling error:', err.message);
    }

    await new Promise(r => setTimeout(r, pollIntervalMs));
  }
}

Tracking Transaction Status

Use the getStatus method to track the status of your cross-chain transfer.
async function trackLiFiStatus(txHash: string, fromChain: string) {
  const request = {
    txHash,
    fromChain,
  };

  try {
    const response = await portal.trading.lifi.getStatus(request);
    const rawResponse = response.data?.rawResponse;

    if (rawResponse) {
      console.log('Status:', rawResponse.status);

      if (rawResponse.transactionId) {
        console.log('Transaction ID:', rawResponse.transactionId);
      }

      if (rawResponse.lifiExplorerLink) {
        console.log('Explorer:', rawResponse.lifiExplorerLink);
      }

      // Check if complete
      switch (rawResponse.status) {
        case 'DONE':
          console.log('Transfer completed successfully!');
          break;
        case 'FAILED':
          console.error('Transfer failed');
          break;
        default:
          console.log('Transfer in progress...');
      }
    }
  } catch (error) {
    console.error('Error getting status:', error.message);
  }
}

Polling for Cross-Chain Completion

For cross-chain transfers, poll the status endpoint until the transfer completes:
async function pollForCompletion(
  txHash: string,
  fromChain: string,
  maxAttempts: number = 300,
  pollIntervalMs: number = 2000
): Promise<boolean> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      const request = {
        txHash,
        fromChain,
      };

      const response = await portal.trading.lifi.getStatus(request);
      const rawResponse = response.data?.rawResponse;

      if (rawResponse) {
        console.log(`Polling (${attempt + 1}/${maxAttempts}): ${rawResponse.status}`);

        switch (rawResponse.status) {
          case 'DONE':
            return true;
          case 'FAILED':
            return false;
          default:
            // Continue polling
            break;
        }
      }
    } catch (error) {
      // Continue polling on error
      console.error('There was an error in a polling intent:', error);
    }

    await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
  }

  return false; // Timeout
}

Example Flow

Here’s a complete example of executing a cross-chain bridge:
// 1. Get user address
const userAddress = await portal.getEip155Address();
if (!userAddress) return;

// 2. Get a quote
const quoteRequest = {
  fromChain: 'eip155:8453', // Base Mainnet
  toChain: 'eip155:42161', // Arbitrum
  fromToken: 'ETH',
  toToken: 'USDC',
  fromAddress: userAddress,
  fromAmount: '100000000000000', // 0.0001 ETH (in wei)
};

const quoteResponse = await portal.trading.lifi.getQuote(quoteRequest);

let txHash: string | null = null;

const quote = quoteResponse.data?.rawResponse;
const transactionRequest = quote?.transactionRequest;

if (!transactionRequest) {
  console.error('No quote available');
  return;
}

// 3. Extract transaction parameters
const from = transactionRequest.from;
const to = transactionRequest.to;

if (!from || !to) {
  console.error('Missing required fields');
  return;
}

const value = transactionRequest.value || '0x0';
const data = transactionRequest.data || '';

// 4. Sign and submit the transaction
const ethTransaction = {
  from,
  to,
  value,
  data,
};

const sendResponse = await portal.request(quoteRequest.fromChain, 'eth_sendTransaction', [ethTransaction]);

txHash = sendResponse.result as string;

if (!txHash) {
  console.error('Failed to submit transaction');
  return;
}

console.log('Transaction submitted:', txHash);

// 5. Track status for cross-chain completion
if (txHash) {
  const completed = await pollForCompletion(txHash, quoteRequest.fromChain);

  if (completed) {
    console.log('Bridge completed successfully!');
  } else {
    console.error('Bridge failed or timed out');
  }
}

Best Practices

  1. Compare quotes/routes before signing and submitting the transaction(s) to find the best option for your use case
  2. Process steps sequentially for multi-step routes, ensuring each step completes before starting the next
  3. Handle network errors gracefully and provide user feedback
  4. Monitor transaction status for cross-chain transfers, as they may take longer than single-chain transactions
  5. Validate user balances before initiating swaps or bridges

Supported Networks

Li.Fi supports a wide range of networks for bridging and swapping. Common networks include:
  • Monad (eip155:143)
  • Ethereum (eip155:1)
  • Polygon (eip155:137)
  • Base (eip155:8453)
  • Arbitrum (eip155:42161)
  • Optimism (eip155:10)
  • Avalanche (eip155:43114)
  • And many more…
For the complete list of supported networks, refer to the Li.Fi documentation.

Next Steps