> ## 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.

# Bridge & Swap with Li.Fi

> Learn how to bridge and swap tokens across multiple chains using Portal's Web SDK with Li.Fi integration.

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](./create-a-wallet))
* Li.Fi integration enabled in your Portal Dashboard (see [Li.Fi Integration](../../../integrations/Trading/lifi))

<Note>
  **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.
</Note>

## 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

```typescript theme={null}
tradeAsset(
  params: LifiTradeAssetParams,
  options?: LifiTradeAssetOptions,
): Promise<LifiTradeAssetResult>
```

**Essential parameters**

| Name           | Required | Description                                                                                                                                                     |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fromChain`    | Yes      | Source chain, CAIP-2 (e.g. `'eip155:8453'`).                                                                                                                    |
| `toChain`      | Yes      | Destination chain, CAIP-2.                                                                                                                                      |
| `fromToken`    | Yes      | Source token symbol or address.                                                                                                                                 |
| `toToken`      | Yes      | Destination token symbol or address.                                                                                                                            |
| `amount`       | Yes      | Amount in smallest units (e.g. wei).                                                                                                                            |
| `fromAddress`  | Yes      | Sender address.                                                                                                                                                 |
| `toAddress`    | No       | Recipient; defaults to `fromAddress`.                                                                                                                           |
| `routeOptions` | No       | Slippage, ordering, bridges, etc. See [Route options](#route-options).                                                                                          |
| `routeIndex`   | No       | Route index from discovery; default `0`.                                                                                                                        |
| `onProgress`   | No       | Fired for each major stage (`fetching_routes`, `signing`, `submitted`, `confirming`, `lifi_pending`, `complete`, `failed`, …).                                  |
| `statusPoll`   | No       | Overrides the **built-in Li.Fi status polling** used **after** on-chain confirmation for each step (same shape as [`pollStatus` options](#pollstatus-options)). |

Second argument `LifiTradeAssetOptions`:

| Name                     | Required | Description                                                                                                                                                                                                                                                                                                                           |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `signAndSendTransaction` | No       | Per-call signer override for route-step transactions.                                                                                                                                                                                                                                                                                 |
| `waitForConfirmation`    | No       | Per-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. |
| `evmRequestFn`           | No       | **Web SDK:** Per-call EVM RPC function used by the built-in fallback EVM receipt poller when `waitForConfirmation` is not provided.                                                                                                                                                                                                   |
| `evmPollerOptions`       | No       | Per-call tuning for the built-in EVM receipt poller (`pollIntervalMs`, `timeoutMs`) when `waitForConfirmation` is not provided.                                                                                                                                                                                                       |

<Warning>
  `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.
</Warning>

**Return value**

| Field    | Description                           |
| -------- | ------------------------------------- |
| `hashes` | Transaction hashes per executed step. |
| `steps`  | Step objects from the API.            |
| `route`  | The executed route.                   |

**Example (progress + per-call confirmation options)**

```typescript theme={null}
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.

```typescript theme={null}
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).

```typescript theme={null}
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

| Name                   | Default                             | Description                                                                                                                                             |
| ---------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `everyMs`              | `10000`                             | Time between requests (ms).                                                                                                                             |
| `initialDelayMs`       | `10000`                             | Delay before the first request (ms). Same default when `tradeAsset` forwards `statusPoll`: omitted fields keep these defaults unless you override them. |
| `timeoutMs`            | `600000`                            | Max total poll time.                                                                                                                                    |
| `maxConsecutiveErrors` | `10`                                | Abort after this many consecutive hard errors.                                                                                                          |
| `backoff`              | `factor` 1.5, `maxIntervalMs` 15000 | Backoff 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:

| Name                   | Type                      | Description                                                       |
| ---------------------- | ------------------------- | ----------------------------------------------------------------- |
| `slippage`             | `number`                  | Maximum acceptable slippage as a decimal (e.g. `0.005` for 0.5%). |
| `order`                | `'FASTEST' \| 'CHEAPEST'` | Route ordering preference.                                        |
| `insurance`            | `boolean`                 | Whether to include bridge insurance.                              |
| `bridges`              | `LifiToolsConfiguration`  | Allow/deny specific bridge tools.                                 |
| `exchanges`            | `LifiToolsConfiguration`  | Allow/deny specific exchange tools.                               |
| `allowSwitchChain`     | `boolean`                 | Allow routes that require switching chains mid-route.             |
| `allowDestinationCall` | `boolean`                 | Allow contract calls on the destination chain.                    |
| `fee`                  | `number`                  | Integrator fee percentage.                                        |
| `maxPriceImpact`       | `number`                  | Maximum price impact as a decimal.                                |
| `referrer`             | `string`                  | Referrer 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.

```typescript theme={null}
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.

```typescript theme={null}
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).

```typescript theme={null}
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`:

```typescript theme={null}
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);
    }
  }
}
```

<Note>
  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](./delegations) guide.
</Note>

### Signing and Submitting Transactions

```typescript theme={null}
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);
  }
}
```

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

### Processing Multiple Route Steps

For routes with multiple steps, process them sequentially:

```typescript theme={null}
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

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

```typescript theme={null}
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.

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
// 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](https://docs.li.fi). If you need a chain that isn't listed above, contact Portal support.

<Note>
  **Testnets are not supported.**
</Note>

## Next Steps

* Learn about [signing transactions](./sign-a-transaction)
* Explore [sending tokens](./send-tokens)
* Check out [Portal API methods](./portal-api-methods)
* See the [Client API Li.Fi endpoints](../../../apis/client/manual-reference#lifi-integration)
