Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt

Use this file to discover all available pages before exploring further.

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

Overview

The Li.Fi functionality allows you to:
  • Run an end-to-end trade with tradeAsset (routes, steps, signing, confirmation, and Li.Fi status polling)
  • 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
  • Poll Li.Fi status with pollStatus when you already have a transaction hash

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)
Supported Chains: Li.Fi integration currently supports EVM-compatible chains (Ethereum, Base, Arbitrum, Polygon, etc.) and Solana. Other non-EVM chains may not be supported for Li.Fi operations.Solana vs evmRequestFn: The built-in fallback that watches EVM receipts (evmRequestFn + receipt polling) only runs when the step’s network is an eip155:* chain. For Solana (or any non-EVM) step, that path does not apply — you must have waitForConfirmation (the default when using the standard Portal client is enough). If you use LiFi without Portal’s defaults, supply waitForConfirmation yourself for those steps; relying on evmRequestFn alone will throw once a non-EVM step needs confirmation.

High-Level Methods

tradeAsset

Runs the end-to-end Li.Fi flow in one call:
  1. Discover routes (getRoutes)
  2. Select a route (routeIndex, default 0)
  3. Build each step (getRouteStep)
  4. Sign and broadcast each step transaction
  5. Wait for on-chain confirmation for that step
  6. Poll Li.Fi status until terminal for that step
  7. Continue to the next step
Steps are executed sequentially (in order), not in parallel.

Strict Confirmation Contract

Li.Fi enforces a strict confirmation contract. Each step MUST be confirmed on-chain before proceeding to the next step:
  • Success: waitForConfirmation returns true or void
  • Failure: Any other value (false, undefined), timeout, unsupported network, or thrown error
When confirmation fails, the entire tradeAsset call throws and no further steps are executed. This ensures no silent failures or partial execution. Default timeout behavior:
  • Timeout: 900_000ms (15 minutes)
  • Poll interval: 4_000ms
  • Unsupported networks return false (treated as failure)
There is no optimistic fallback.

Signature

tradeAsset(
  params: LifiTradeAssetParams,
  options?: LifiTradeAssetOptions,
): Promise<LifiTradeAssetResult>
Essential parameters
NameRequiredDescription
fromChainYesSource chain, CAIP-2 (e.g. 'eip155:8453').
toChainYesDestination chain, CAIP-2.
fromTokenYesSource token symbol or address.
toTokenYesDestination token symbol or address.
amountYesAmount in smallest units (e.g. wei).
fromAddressYesSender address.
toAddressNoRecipient; defaults to fromAddress.
routeOptionsNoSlippage, ordering, bridges, etc. See Route options.
routeIndexNoRoute index from discovery; default 0.
onProgressNoFired for each major stage (fetching_routes, signing, submitted, confirming, lifi_pending, complete, failed, …).
statusPollNoOverrides the built-in Li.Fi status polling used after on-chain confirmation for each step (same shape as pollStatus options).
Second argument LifiTradeAssetOptions:
NameRequiredDescription
signAndSendTransactionNoPer-call signer override for route-step transactions.
waitForConfirmationNoPer-call confirmation override. Called after each submitted step with (txHash, network). MUST return true or resolve for success. Returning false, throwing an error, or timing out will abort the entire flow and throw an error. When omitted, the built-in EVM receipt poller is used with the same strict behavior.
evmRequestFnNoWeb SDK: Per-call EVM RPC function used by the built-in fallback EVM receipt poller when waitForConfirmation is not provided.
evmPollerOptionsNoPer-call tuning for the built-in EVM receipt poller (pollIntervalMs, timeoutMs) when waitForConfirmation is not provided.
tradeAsset requires at least one confirmation mechanism. If neither waitForConfirmation nor evmRequestFn is available (from instance defaults or per-call options), the method throws immediately:[LiFi] tradeAsset requires waitForConfirmation or evmRequestFn fallback.When you construct Portal with an rpcConfig, evmRequestFn is wired automatically — no extra setup needed. This requirement only surfaces when using LiFi standalone or without Portal’s default wiring.On non-EVM networks (for example Solana), evmRequestFn is never used for confirmation — only waitForConfirmation applies. Use the standard Portal instance (which sets both the signer and waitForConfirmation), or pass waitForConfirmation in options for those routes.Receipt-polling timeouts are strict: if confirmation times out, the entire tradeAsset call throws and does not continue to bridge-status polling or subsequent steps.
Return value
FieldDescription
hashesTransaction hashes per executed step.
stepsStep objects from the API.
routeThe executed route.
Example (progress + per-call confirmation options)
import Portal from '@portal-hq/web'
import type { LifiTradeAssetParams, LifiTradeAssetOptions } from '@portal-hq/web'

const portal = new Portal({
  apiKey: 'YOUR_PORTAL_CLIENT_API_KEY',
  rpcConfig: {
    'eip155:8453': 'https://YOUR_RPC_URL',
    'eip155:42161': 'https://YOUR_RPC_URL',
  },
})

async function runTrade() {
  const fromAddress = await portal.getEip155Address()
  if (!fromAddress) throw new Error('No EVM address')

  const params: LifiTradeAssetParams = {
    fromChain: 'eip155:8453',
    toChain: 'eip155:42161',
    fromToken: 'ETH',
    toToken: 'USDC',
    amount: '1000000000000',
    fromAddress,
    statusPoll: {
      everyMs: 10_000,
      initialDelayMs: 10_000,
      timeoutMs: 600_000,
    },
    onProgress: (status, data) => {
      console.log('[Li.Fi]', status, data?.txHash ?? '')
    },
  }

  const options: LifiTradeAssetOptions = {
    waitForConfirmation: async (txHash, network) => {
      return portal.waitForConfirmation(txHash, network)
    },
    evmPollerOptions: { pollIntervalMs: 4_000, timeoutMs: 600_000 },
  }

  try {
    const result = await portal.trading.lifi.tradeAsset(params, options)
    console.log('Hashes:', result.hashes)
    console.log('Executed steps:', result.steps.length)
    console.log('Route id:', result.route.id)
    return result
  } catch (e) {
    console.error('tradeAsset failed', e)
    throw e
  }
}
Example (default confirmation behavior) When you construct Portal, Li.Fi receives the same default signAndSendTransaction, waitForConfirmation, and evmRequestFn as other trading helpers — you can call tradeAsset with only params if that wiring is sufficient for your chain and RPC setup.
const result = await portal.trading.lifi.tradeAsset({
  fromChain: 'eip155:8453',
  toChain: 'eip155:42161',
  fromToken: 'ETH',
  toToken: 'USDC',
  amount: '1000000000000',
  fromAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
})
Wrap tradeAsset in try/catch; failures (no route, step error, receipt timeout, Li.Fi terminal FAILED, …) throw after onProgress may report 'failed'.

Progress lifecycle

onProgress can emit statuses such as:
  • fetching_routes
  • route_selected
  • preparing_step
  • signing
  • submitted
  • confirming
  • lifi_pending
  • step_done
  • complete
  • failed

pollStatus

Built-in Li.Fi status polling with retries and backoff. Use when you already have a tx hash (for example from a manual flow) and want the same polling behavior as inside tradeAsset, without implementing the loop yourself. On Web, pass the update callback as onUpdate inside the second-argument options object (React Native passes it as a separate argument).
import Portal from '@portal-hq/web'

async function pollTransferStatus(portal: Portal, txHash: string) {
  const terminal = await portal.trading.lifi.pollStatus(
    {
      txHash,
      fromChain: 'eip155:8453',
      toChain: 'eip155:42161',
    },
    {
      onUpdate: (statusUpdate) => {
        console.log('Status:', statusUpdate.status)
        return true
      },
      everyMs: 10_000,
      timeoutMs: 600_000,
    },
  )

  console.log('Final status:', terminal.status)
}

pollStatus options

NameDefaultDescription
everyMs10000Time between requests (ms).
initialDelayMs10000Delay before the first request (ms). Same default when tradeAsset forwards statusPoll: omitted fields keep these defaults unless you override them.
timeoutMs600000Max total poll time.
maxConsecutiveErrors10Abort after this many consecutive hard errors.
backofffactor 1.5, maxIntervalMs 15000Backoff between polls.
Return false from onUpdate to stop early; return true (or nothing) to continue.

Route options

The routeOptions parameter on tradeAsset (and the options field on LifiRoutesRequest) accepts a LifiRoutesRequestOptions object:
NameTypeDescription
slippagenumberMaximum acceptable slippage as a decimal (e.g. 0.005 for 0.5%).
order'FASTEST' | 'CHEAPEST'Route ordering preference.
insurancebooleanWhether to include bridge insurance.
bridgesLifiToolsConfigurationAllow/deny specific bridge tools.
exchangesLifiToolsConfigurationAllow/deny specific exchange tools.
allowSwitchChainbooleanAllow routes that require switching chains mid-route.
allowDestinationCallbooleanAllow contract calls on the destination chain.
feenumberIntegrator fee percentage.
maxPriceImpactnumberMaximum price impact as a decimal.
referrerstringReferrer address for fee sharing.

Low-level methods

Use these methods when you want full control over quotes, route selection, signing, and status polling instead of calling tradeAsset.

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.

Approving ERC-20 Tokens

If your fromToken is an ERC-20, the Li.Fi router cannot move it on your behalf until you grant an on-chain allowance. Skip this step when the fromToken is the chain’s native asset (its address is 0x0000000000000000000000000000000000000000). Build the approval transaction with the portal.delegations.approve method, then sign each transaction it returns with the same eth_sendTransaction flow used to sign the swap. Call this helper after obtaining a quote and before calling executeTransaction:
const NATIVE_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000';

async function approveErc20IfNeeded(
  quote: LifiQuoteResponse,
  fromAmount: string,
  fromChainId: string,
): Promise<void> {
  const action = quote.data?.rawResponse?.action;
  const estimate = quote.data?.rawResponse?.estimate;
  if (!action || !estimate) return;

  const fromToken = action.fromToken;
  if (!fromToken || fromToken.address === NATIVE_TOKEN_ADDRESS) {
    // Native asset — no approval needed.
    return;
  }

  // Convert the raw fromAmount into the token's primary denomination
  // (e.g. raw "10000" with 6 decimals → "0.01"). Use a string-aware
  // conversion to preserve precision for large values.
  const amount = formatUnits(fromAmount, fromToken.decimals); // e.g. ethers.formatUnits or viem.formatUnits

  const { transactions } = await portal.delegations.approve({
    chain: fromChainId,
    token: fromToken.address,
    delegateAddress: estimate.approvalAddress,
    amount,
  });

  for (const tx of transactions) {
    const sendResponse = await portal.request({
      chainId: fromChainId,
      method: 'eth_sendTransaction',
      params: [tx],
    });
    const txHash = sendResponse.result as string;
    if (txHash) {
      await waitForConfirmation(txHash, fromChainId);
    }
  }
}
This step only applies when the fromToken is an ERC-20. Native-asset swaps (ETH, MATIC, etc.) skip it. For more on the delegations API, see the Manage Token Delegations guide.

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 txHash = await portal.request({
      chainId,
      method: 'eth_sendTransaction',
      params: [ethTransaction],
    })
    if (txHash) {
      console.log('Transaction submitted:', txHash);

      // Wait for on-chain confirmation (throws on failure)
      await waitForConfirmation(txHash, chainId);
      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

The example below is for educational purposes showing low-level receipt polling. When using tradeAsset, confirmation failures automatically throw and abort the entire operation. The strict confirmation contract ensures no silent failures.
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. Approve the fromToken if it's an ERC-20 (no-op for native assets)
await approveErc20IfNeeded(quoteResponse, quoteRequest.fromAmount, quoteRequest.fromChain);

// 4. 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 || '';

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

txHash = await portal.request({
  chainId: quoteRequest.fromChain,
  method: 'eth_sendTransaction',
  params: [ethTransaction],
})

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

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

// 6. 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

Portal’s Li.Fi integration supports the following mainnet networks:
  • Monad (eip155:143)
  • Ethereum (eip155:1)
  • Optimism (eip155:10)
  • BSC (eip155:56)
  • Gnosis (eip155:100)
  • Unichain (eip155:130)
  • Polygon (eip155:137)
  • Sonic (eip155:146)
  • Mantle (eip155:5000)
  • Base (eip155:8453)
  • Arbitrum (eip155:42161)
  • Celo (eip155:42220)
  • Avalanche (eip155:43114)
  • Linea (eip155:59144)
  • Berachain (eip155:80094)
  • Katana (eip155:747474)
  • Solana (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp)
  • Bitcoin (bip122:000000000019d6689c085ae165831e93-p2wpkh)
For the complete list of networks Li.Fi supports across its ecosystem, refer to the Li.Fi documentation. If you need a chain that isn’t listed above, contact Portal support.
Testnets are not supported.

Next Steps