> ## 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 Android SDK with Li.Fi integration.

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

## High-Level Methods

`tradeAsset` runs the entire bridge or swap in one call. `pollStatus` exposes the same Li.Fi status poller `tradeAsset` uses internally, for manual flows where you already have a transaction hash.

If you only need to move tokens, use `tradeAsset`. Reach for the [low-level methods](#low-level-methods) when you need to inspect routes before committing, run your own signing, or drive a custom UI.

Both return `Result<T>` like every other method on this page — handle them with `onSuccess` / `onFailure`, not `try/catch`.

### tradeAsset

Runs the end-to-end Li.Fi flow:

1. Discover routes (`getRoutes`)
2. Select a route (`routeIndex`, default `0`)
3. Build each step (`getRouteStep`)
4. Sign and broadcast that step's transaction
5. Wait for on-chain confirmation of that step
6. Poll Li.Fi status until the step reaches a terminal state
7. Continue to the next step

Steps execute **sequentially**, never in parallel. Signing and confirmation for each step happen on that step's own chain, which the SDK resolves from the step itself — so a multi-chain route signs on each chain in turn without you managing it.

**Confirmation is strict.** Every step must confirm on-chain before the next begins. `waitForConfirmation` must return `true`; anything else aborts the whole trade and yields a failed `Result`. There is no optimistic fallback.

### Signature

```kotlin theme={null}
open suspend fun tradeAsset(params: LifiTradeAssetParams): Result<LifiTradeAssetResult>
```

**Essential parameters**

| Parameter     | Type                   | Required        | Description                                                                                       |
| ------------- | ---------------------- | --------------- | ------------------------------------------------------------------------------------------------- |
| `fromChain`   | `String`               | Yes             | Source chain. Use CAIP-2 (`"eip155:8453"`), as everywhere else in this guide.                     |
| `toChain`     | `String`               | Yes             | Destination chain, same format.                                                                   |
| `fromToken`   | `String`               | Yes             | Source token contract address or symbol.                                                          |
| `toToken`     | `String`               | Yes             | Destination token contract address or symbol.                                                     |
| `amount`      | `String`               | Yes             | Amount in the token's base units, as an integer string (for example wei for an 18-decimal token). |
| `fromAddress` | `String?`              | No, but pass it | Sending wallet address. See the note below.                                                       |
| `toAddress`   | `String?`              | No              | Receiving wallet address. Falls back to `fromAddress` when omitted.                               |
| `routeIndex`  | `Int?`                 | No              | Which discovered route to execute. Default `0`.                                                   |
| `onProgress`  | `LifiProgressHandler?` | No              | Fired at each stage. See [Progress lifecycle](#progress-lifecycle).                               |

<Warning>
  `fromAddress` is nullable in the type system but the SDK **does not fill it in for you** — it is forwarded to Li.Fi exactly as given. Omitting it means routes are quoted without a sender, while the transaction is still signed by your Portal wallet, so the quote may not match what actually executes. Pass it explicitly, resolving it from the chain you are trading on with `portal.getAddress(fromChain)`.
</Warning>

**Configuring the signer and confirmation**

Unlike the React Native and Web SDKs, `tradeAsset` takes **no second options argument**. The signing and confirmation hooks are injected once, when the `Lifi` instance is constructed:

```kotlin theme={null}
open class Lifi(
    private val api: LifiTradingApi,
    private val signAndSendTransaction: LifiSignAndSendTransaction? = null,
    private val waitForConfirmation: LifiWaitForConfirmation? = null,
    private val stepPollOptions: LifiPollStatusOptions = LifiPollStatusOptions(
        everyMs = 10_000L,
        initialDelayMs = 10_000L,
        timeoutMs = 600_000L
    )
)
```

`Portal` wires both automatically — `signAndSendTransaction` via `eth_sendTransaction`, and `waitForConfirmation` via an internal receipt poller that retries up to 30 times — so `portal.trading.lifi.tradeAsset(params)` works with no setup.

The two callbacks are:

```kotlin theme={null}
typealias LifiSignAndSendTransaction = suspend (transaction: EthTransactionParam, chainId: String) -> String
typealias LifiWaitForConfirmation = suspend (txHash: String, chainId: String) -> Boolean
```

`waitForConfirmation` **must return `true` for a confirmed transaction.** Both failure modes abort the trade, but they surface differently:

* **Returning `false`** fails the `Result` with `LifiTradeAssetException.TransactionConfirmationFailed(txHash)`.
* **Throwing** propagates through `tradeAsset`'s catch-all and fails the `Result` with **the original exception**, not `TransactionConfirmationFailed`.

That distinction matters for the closure `Portal` wires in: its receipt poller returns `false` for a reverted or never-confirmed transaction, but it **throws** when the RPC calls themselves keep failing — a bad chain ID, an unreachable RPC, an auth error. An `onFailure` that only matches `TransactionConfirmationFailed` will silently miss that whole class of failure, so always keep an `else` branch that surfaces `error` as-is.

**Overriding the defaults.** `portal.trading` is built lazily by `Portal` with its own closures already supplied, so the instance at `portal.trading.lifi` cannot be reconfigured after the fact. To use different behavior, build your own instance and call `tradeAsset` on that instead.

`Trading` accepts the two callbacks and constructs its `Lifi` internally — it does not take a `Lifi`:

```kotlin theme={null}
class Trading(
    private val api: Api,
    signAndSendTransaction: LifiSignAndSendTransaction? = null,
    waitForConfirmation: LifiWaitForConfirmation? = null
)

// Custom callbacks, default step polling
val trading = Trading(
    api = portal.api,
    signAndSendTransaction = { transaction, chainId -> mySigner.signAndSend(transaction, chainId) },
    waitForConfirmation = { txHash, chainId -> myConfirmer.await(txHash, chainId) }
)
val result = trading.lifi.tradeAsset(params)
```

Construct `Lifi` directly when you also need to change the per-step polling, which `Trading` does not expose:

```kotlin theme={null}
val lifi = Lifi(
    api = portal.api.lifi,
    signAndSendTransaction = { transaction, chainId -> mySigner.signAndSend(transaction, chainId) },
    waitForConfirmation = { txHash, chainId -> myConfirmer.await(txHash, chainId) },
    stepPollOptions = LifiPollStatusOptions(everyMs = 5_000L, initialDelayMs = 0L, timeoutMs = 300_000L)
)
val result = lifi.tradeAsset(params)
```

<Note>
  Because `Trading` does not pass `stepPollOptions` through, the per-step Li.Fi polling inside `tradeAsset` always uses the `Lifi` constructor default — a 10-second initial delay, 10-second interval, 10-minute timeout. That default differs from the standalone `pollStatus` default, which has **no** initial delay.
</Note>

<Note>
  `Lifi`, `LifiTradingApi`, and their methods are `open` as of 9.1.0, so you can subclass them to stub Li.Fi in tests.
</Note>

**Return value**

| Field    | Type             | Description                                                                 |
| -------- | ---------------- | --------------------------------------------------------------------------- |
| `hashes` | `List<String>`   | One transaction hash per executed step, in execution order.                 |
| `steps`  | `List<LifiStep>` | The enriched steps that were executed, with `transactionRequest` populated. |
| `route`  | `LifiRoute`      | The route that was selected and executed.                                   |

### Example (progress reporting)

```kotlin theme={null}
import io.portalhq.android.api.data.lifi.LifiTradeAssetParams
import io.portalhq.android.api.data.lifi.LifiTradeAssetProgressStatus

lifecycleScope.launch {
    val address = portal.getAddress("eip155:8453") ?: return@launch

    portal.trading.lifi.tradeAsset(
        LifiTradeAssetParams(
            fromChain = "eip155:8453",
            toChain = "eip155:42161",
            fromToken = "ETH",
            toToken = "USDC",
            amount = "100000000000000", // 0.0001 ETH (in wei)
            fromAddress = address,
            onProgress = { status, data ->
                when (status) {
                    LifiTradeAssetProgressStatus.SIGNING ->
                        Log.i("Portal", "Signing step ${(data.stepIndex ?: 0) + 1} of ${data.totalSteps ?: 0}")
                    LifiTradeAssetProgressStatus.SUBMITTED ->
                        Log.i("Portal", "Submitted: ${data.txHash}")
                    LifiTradeAssetProgressStatus.COMPLETE ->
                        Log.i("Portal", "Trade complete")
                    LifiTradeAssetProgressStatus.FAILED ->
                        Log.e("Portal", "Failed: ${data.errorMessage}")
                    else -> Unit
                }
            }
        )
    ).onSuccess { result ->
        Log.i("Portal", "Hashes: ${result.hashes}")
        Log.i("Portal", "Executed steps: ${result.steps.size}")
    }.onFailure { error ->
        when (error) {
            is LifiTradeAssetException.NoRoutesFound ->
                Log.e("Portal", "No route available for this pair")
            is LifiTradeAssetException.TransactionConfirmationFailed ->
                // Reverted, or never confirmed within the retry budget — check the hash on-chain.
                Log.e("Portal", "Not confirmed: ${error.message}", error)
            else ->
                // Catches thrown confirmation errors (persistent RPC failures) too.
                Log.e("Portal", "tradeAsset failed: ${error.message}", error)
        }
    }
}
```

### Example (minimal)

```kotlin theme={null}
lifecycleScope.launch {
    val address = portal.getAddress("eip155:8453") ?: return@launch

    portal.trading.lifi.tradeAsset(
        LifiTradeAssetParams(
            fromChain = "eip155:8453",
            toChain = "eip155:42161",
            fromToken = "ETH",
            toToken = "USDC",
            amount = "100000000000000",
            fromAddress = address
        )
    ).onSuccess { result ->
        Log.i("Portal", "Hashes: ${result.hashes}")
    }.onFailure { error ->
        Log.e("Portal", "tradeAsset failed", error)
    }
}
```

### Errors

Failures arrive inside the returned `Result` as a `LifiTradeAssetException`:

| Case                                    | When                                                                                                                              |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `MissingSigner`                         | No signing closure on the `Lifi` instance. Cannot occur on `portal.trading.lifi`.                                                 |
| `MissingConfirmation`                   | No confirmation closure on the `Lifi` instance. Cannot occur on `portal.trading.lifi`.                                            |
| `ApiError(detail)`                      | Li.Fi returned an error payload while fetching routes, resolving a step, or polling status. `detail` carries the API message.     |
| `NoRoutesFound`                         | Li.Fi returned no routes for the requested trade.                                                                                 |
| `RouteIndexOutOfBounds`                 | `routeIndex` is negative or beyond the number of discovered routes.                                                               |
| `RouteHasNoSteps`                       | The selected route contains no steps.                                                                                             |
| `MissingTransactionRequest`             | A step came back without a transaction request to sign.                                                                           |
| `InvalidTransactionRequest`             | A step's transaction request was missing required fields, or held a value that could not be parsed into a valid RPC hex quantity. |
| `TransactionConfirmationFailed(txHash)` | The confirmation closure returned `false`. Not necessarily a revert — see the note below.                                         |
| `LifiTransferFailed(detail)`            | Li.Fi reported a `FAILED` terminal state.                                                                                         |
| `PollTimeout(cause)`                    | Status polling exceeded the timeout. `cause` carries the most recent transient error, when there was one.                         |
| `PollFailed(cause)`                     | Polling hit `maxConsecutiveErrors` consecutive failures and stopped early. `cause` carries the triggering error.                  |

`MissingSigner`, `MissingConfirmation`, `NoRoutesFound`, `RouteIndexOutOfBounds`, `RouteHasNoSteps`, `MissingTransactionRequest`, and `InvalidTransactionRequest` are `object` singletons — match them with `is`. The rest are classes carrying detail.

<Warning>
  **`TransactionConfirmationFailed` is not proof the transaction failed on-chain.** Portal's receipt poller returns `false` in two different situations, and Android maps both onto this one case:

  * The receipt came back reverted (`status: 0x0`) — the transaction genuinely failed.
  * The retries ran out before any receipt appeared — the transaction may still be **pending** and could yet confirm.

  Treat it as "not confirmed", not "failed" — re-check the hash on-chain rather than reporting a definitive failure. Note that `txHash` is a constructor parameter, not an exposed property: it is interpolated into `error.message`, so capture the hash from the `SUBMITTED` progress event if you need it programmatically.

  iOS keeps these two outcomes apart as `transactionConfirmationFailed` and `transactionConfirmationTimedOut`, so cross-platform code cannot assume both SDKs report an unconfirmed transaction the same way.
</Warning>

<Note>
  Cancelling the enclosing coroutine throws `CancellationException` rather than returning a failed `Result`, and **no `FAILED` progress event is emitted**. This preserves structured concurrency, but it means a UI that only dismisses its progress state on `FAILED` or `COMPLETE` will hang on cancellation — handle `CancellationException` separately.
</Note>

### Progress lifecycle

`onProgress` receives a `LifiTradeAssetProgressStatus` and a `LifiTradeAssetProgressData`. Every field on the data class is nullable; which ones are populated depends on the stage:

| Status            | `value`           | Populated data                                   |
| ----------------- | ----------------- | ------------------------------------------------ |
| `FETCHING_ROUTES` | `fetching_routes` | —                                                |
| `ROUTE_SELECTED`  | `route_selected`  | `routeIndex`, `route`, `totalSteps`              |
| `PREPARING_STEP`  | `preparing_step`  | `routeIndex`, `stepIndex`, `totalSteps`, `route` |
| `SIGNING`         | `signing`         | above, plus `step`                               |
| `SUBMITTED`       | `submitted`       | above, plus `txHash`                             |
| `CONFIRMING`      | `confirming`      | above, plus `txHash`                             |
| `LIFI_PENDING`    | `lifi_pending`    | above, plus `lifiStatus` on later emissions      |
| `STEP_DONE`       | `step_done`       | above                                            |
| `COMPLETE`        | `complete`        | `route`, `totalSteps`                            |
| `FAILED`          | `failed`          | `errorMessage`                                   |

`txHash` is `null` until `SUBMITTED`. `errorMessage` is only ever set on `FAILED`.

### pollStatus

Polls Li.Fi for the status of a transfer until it reaches a terminal state. Use it when you have submitted a transaction yourself and want the same polling behavior `tradeAsset` uses internally.

```kotlin theme={null}
open suspend fun pollStatus(
    request: LifiStatusRequest,
    onUpdate: ((LifiStatusRawResponse) -> Boolean)? = null,
    options: LifiPollStatusOptions = LifiPollStatusOptions()
): Result<LifiStatusRawResponse>
```

Both `onUpdate` and `options` are defaulted, so `pollStatus(request)` alone is valid.

Returning `false` from `onUpdate` stops polling early and succeeds with the last status received — it is not an error. Returning `true` continues.

```kotlin theme={null}
lifecycleScope.launch {
    portal.trading.lifi.pollStatus(
        request = LifiStatusRequest(
            txHash = txHash,
            fromChain = "eip155:8453",
            toChain = "eip155:42161"
        ),
        onUpdate = { update ->
            Log.i("Portal", "Status: ${update.status}")
            true // return false to stop polling early
        }
    ).onSuccess { final ->
        Log.i("Portal", "Final status: ${final.status}")
    }.onFailure { error ->
        Log.e("Portal", "pollStatus failed", error)
    }
}
```

### pollStatus options

| Option                 | Type   | Default    | Description                                                                                                                                                                                 |
| ---------------------- | ------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `everyMs`              | `Long` | `10_000L`  | Interval between polls, in milliseconds.                                                                                                                                                    |
| `initialDelayMs`       | `Long` | `0L`       | Delay before the first poll, in milliseconds. Note the `Lifi` constructor's `stepPollOptions` default uses `10_000L` instead.                                                               |
| `timeoutMs`            | `Long` | `600_000L` | Overall polling timeout. Exceeding it fails with `PollTimeout`.                                                                                                                             |
| `maxConsecutiveErrors` | `Int`  | `5`        | Consecutive transient errors tolerated before failing fast with `PollFailed`. Stops a persistent hard error — an auth failure, say — from being retried silently until the timeout elapses. |

```kotlin theme={null}
portal.trading.lifi.pollStatus(
    request = LifiStatusRequest(txHash = txHash, fromChain = "eip155:8453", toChain = "eip155:42161"),
    options = LifiPollStatusOptions(
        everyMs = 5_000L,
        initialDelayMs = 10_000L,
        timeoutMs = 300_000L,
        maxConsecutiveErrors = 3
    )
)
```

***

## Low-Level Methods

The rest of this guide covers the individual Li.Fi methods. Use them when you need control over route selection, signing, or status tracking that `tradeAsset` does not expose.

## Getting a Quote

Use the `getQuote` method to get a quote for bridging or swapping tokens across chains.

```kotlin theme={null}
lifecycleScope.launch {
    val address = portal.getAddress("eip155:1") ?: return@launch

    val request = LifiQuoteRequest(
        fromChain = "eip155:8453", // Base Mainnet
        toChain = "eip155:42161", // Arbitrum
        fromToken = "ETH",
        toToken = "USDC",
        fromAddress = address,
        fromAmount = "100000000000000" // 0.0001 ETH (in wei)
    )

    val response = portal.trading.lifi.getQuote(request)

    response.onSuccess { quoteResponse ->
        val rawResponse = quoteResponse.data?.rawResponse

        if (rawResponse != null) {
            // Process quote response
            rawResponse.estimate?.let { estimate ->
                Log.i("Portal", "From amount: ${estimate.fromAmount}")
                Log.i("Portal", "To amount: ${estimate.toAmount}")
                Log.i("Portal", "Execution duration: ${estimate.executionDuration}s")
            }

            // Sign and submit the transaction if transactionRequest is available
            rawResponse.transactionRequest?.let { transactionRequest ->
                executeTransaction(transactionRequest, request.fromChain)
            }
        }
    }.onFailure { error ->
        Log.e("Portal", "Error getting quote: ${error.message}")
    }
}
```

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.

```kotlin theme={null}
lifecycleScope.launch {
    val address = portal.getAddress("eip155:1") ?: return@launch

    val request = LifiRoutesRequest(
        fromChainId = "eip155:8453", // Base Mainnet
        fromAmount = "100000000000000", // 0.0001 ETH (in wei)
        fromTokenAddress = "ETH",
        toChainId = "eip155:42161", // Arbitrum
        toTokenAddress = "USDC",
        fromAddress = address
    )

    val response = portal.trading.lifi.getRoutes(request)

    response.onSuccess { routesResponse ->
        val rawResponse = routesResponse.data?.rawResponse

        if (rawResponse != null) {
            val routes = rawResponse.routes

            // Find recommended route
            val recommendedRoute = routes.firstOrNull { route ->
                route.tags?.contains("RECOMMENDED") == true
            } ?: routes.firstOrNull()

            recommendedRoute?.let { route ->
                Log.i("Portal", "Selected route: ${route.id}")
                Log.i("Portal", "Steps: ${route.steps.size}")
                Log.i("Portal", "From: ${route.fromAmountUSD} USD")
                Log.i("Portal", "To: ${route.toAmountUSD} USD")

                // Process route steps
                processRouteSteps(route.steps, request.fromChainId)
            }
        }
    }.onFailure { error ->
        Log.e("Portal", "Error getting routes: ${error.message}")
    }
}
```

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

```kotlin theme={null}
suspend fun getStepTransactionDetails(step: LifiStep): LifiStep? {
    return try {
        val response = portal.trading.lifi.getRouteStep(step)
        response.getOrNull()?.data?.rawResponse
    } catch (error: Throwable) {
        Log.e("Portal", "Error getting step details: ${error.message}")
        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(request)` 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`:

```kotlin theme={null}
private const val NATIVE_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000"

suspend fun approveErc20IfNeeded(
    quote: LifiQuoteResponse,
    fromAmount: String,
    fromChainId: String
) {
    val action = quote.data?.rawResponse?.action ?: return
    val estimate = quote.data?.rawResponse?.estimate ?: return
    val fromToken = action.fromToken ?: return

    if (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 BigDecimal-aware
    // conversion to preserve precision for large values.
    val amount = formatUnits(fromAmount, fromToken.decimals)

    val request = ApproveDelegationRequest(
        chain = fromChainId,
        token = fromToken.address,
        delegateAddress = estimate.approvalAddress,
        amount = amount
    )

    portal.delegations.approve(request).onSuccess { response ->
        response.transactions?.forEach { tx ->
            val txDict = mutableMapOf<String, String>(
                "from" to tx.from,
                "to" to tx.to
            )
            tx.data?.let { txDict["data"] = it }
            tx.value?.let { txDict["value"] = it }

            val sendResponse = portal.request(
                chainId = fromChainId,
                method = PortalRequestMethod.eth_sendTransaction,
                params = listOf(txDict),
                options = RequestOptions(signatureApprovalMemo = "Approve token for Li.Fi") // Optional signature approval memo to use for the request
            )

            (sendResponse.result as? String)?.let { txHash ->
                waitForConfirmation(txHash, fromChainId)
            }
        }
    }.onFailure { error ->
        println("Approve delegation failed: ${error.message}")
    }
}
```

<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

```kotlin theme={null}
suspend fun executeTransaction(transactionRequest: JsonElement, chainId: String) {
    try {
        // Parse the JsonElement to extract transaction parameters
        if (!transactionRequest.isJsonObject) {
            Log.e("Portal", "Invalid transaction request")
            return
        }

        val txParamsJson = transactionRequest.asJsonObject

        // Extract required fields
        val from = txParamsJson.get("from")?.asString
        val to = txParamsJson.get("to")?.asString

        if (from == null || to == null) {
            Log.e("Portal", "Missing required 'from' or 'to' field")
            return
        }

        // Extract value (default to 0x0 if not present)
        var value = "0x0"
        txParamsJson.get("value")?.let { valueElement ->
            value = when {
                valueElement.isJsonPrimitive && valueElement.asJsonPrimitive.isString -> valueElement.asString
                valueElement.isJsonPrimitive && valueElement.asJsonPrimitive.isNumber -> {
                    String.format("0x%x", valueElement.asLong)
                }
                else -> "0x0"
            }
        }

        // Extract data
        val data = txParamsJson.get("data")?.asString ?: ""

        // Create transaction
        val ethTransaction = EthTransactionParam(
            from = from,
            to = to,
            value = value,
            data = data,
            gas = null, // Let Portal handle gas estimation
            gasPrice = null,
            maxFeePerGas = null,
            maxPriorityFeePerGas = null
        )

        // Sign and send
        val sendResponse = portal.request(
            chainId = chainId,
            method = PortalRequestMethod.eth_sendTransaction,
            params = listOf(ethTransaction),
            options = RequestOptions(signatureApprovalMemo = "Bridge & swap via Li.Fi") // Optional signature approval memo to use for the request
        )

        val txHash = sendResponse.result as? String
        if (txHash != null) {
            Log.i("Portal", "Transaction submitted: $txHash")

            // Wait for on-chain confirmation
            val confirmed = waitForConfirmation(txHash, chainId)
            if (confirmed) {
                Log.i("Portal", "Transaction confirmed")
            }
        }
    } catch (error: Throwable) {
        Log.e("Portal", "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:

```kotlin theme={null}
suspend fun processRouteSteps(steps: List<LifiStep>, fromChainId: String): Boolean {
    for ((index, step) in steps.withIndex()) {
        Log.i("Portal", "Processing step ${index + 1}/${steps.size}: ${step.tool}")

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

        if (transactionRequest == null) {
            Log.e("Portal", "Failed to get transaction details for step ${index + 1}")
            return false
        }

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

        Log.i("Portal", "Step ${index + 1} completed")
    }

    return true
}
```

### Waiting for Transaction Confirmation

```kotlin theme={null}
suspend fun waitForConfirmation(
    txHash: String,
    chainId: String,
    maxAttempts: Int = 30,
    delayMs: Long = 2000
): Boolean {
    repeat(maxAttempts) {
        kotlinx.coroutines.delay(delayMs)

        try {
            val receiptResponse = portal.request(
                chainId = chainId,
                method = PortalRequestMethod.eth_getTransactionReceipt,
                params = listOf(txHash)
            )

            val receipt = receiptResponse.result as? Map<*, *>
            val status = receipt?.get("status") as? String

            when (status) {
                "0x1" -> return true  // Transaction succeeded
                "0x0" -> return false // Transaction reverted
            }
        } catch (error: Throwable) {
            continue
        }
    }

    return false // Timeout
}
```

## Tracking Transaction Status

Use the `getStatus` method to track the status of your cross-chain transfer.

```kotlin theme={null}
suspend fun trackLiFiStatus(txHash: String, fromChain: String) {
    val request = LifiStatusRequest(
        txHash = txHash,
        fromChain = fromChain
    )

    val response = portal.trading.lifi.getStatus(request)

    response.onSuccess { statusResponse ->
        val rawResponse = statusResponse.data?.rawResponse

        if (rawResponse != null) {
            Log.i("Portal", "Status: ${rawResponse.status}")

            rawResponse.transactionId?.let { txId ->
                Log.i("Portal", "Transaction ID: $txId")
            }

            rawResponse.lifiExplorerLink?.let { explorerLink ->
                Log.i("Portal", "Explorer: $explorerLink")
            }

            // Check if complete
            when (rawResponse.status) {
                LifiTransferStatus.DONE -> {
                    Log.i("Portal", "Transfer completed successfully!")
                }
                LifiTransferStatus.FAILED -> {
                    Log.e("Portal", "Transfer failed")
                }
                else -> {
                    Log.i("Portal", "Transfer in progress...")
                }
            }
        }
    }.onFailure { error ->
        Log.e("Portal", "Error getting status: ${error.message}")
    }
}
```

### Polling for Cross-Chain Completion

For cross-chain transfers, poll the status endpoint until the transfer completes:

```kotlin theme={null}
suspend fun pollForCompletion(
    txHash: String,
    fromChain: String,
    maxAttempts: Int = 300,
    pollIntervalMs: Long = 2000
): Boolean {
    repeat(maxAttempts) { attempt ->
        try {
            val request = LifiStatusRequest(
                txHash = txHash,
                fromChain = fromChain
            )

            val response = portal.trading.lifi.getStatus(request)

            response.onSuccess { statusResponse ->
                val rawResponse = statusResponse.data?.rawResponse

                if (rawResponse != null) {
                    Log.i("Portal", "Polling (${attempt + 1}/$maxAttempts): ${rawResponse.status}")

                    when (rawResponse.status) {
                        LifiTransferStatus.DONE -> return true
                        LifiTransferStatus.FAILED -> return false
                        else -> {} // Continue polling
                    }
                }
            }
        } catch (error: Throwable) {
            // Continue polling on error
        }

        kotlinx.coroutines.delay(pollIntervalMs)
    }

    return false // Timeout
}
```

## Example Flow

Here's a complete example of executing a cross-chain bridge:

```kotlin theme={null}
lifecycleScope.launch {
    try {
        // 1. Get user address
        val userAddress = portal.getAddress("eip155:1") ?: return@launch

        // 2. Get a quote
        val quoteRequest = LifiQuoteRequest(
            fromChain = "eip155:8453", // Base Mainnet
            toChain = "eip155:42161", // Arbitrum
            fromToken = "ETH",
            toToken = "USDC",
            fromAddress = userAddress,
            fromAmount = "100000000000000" // 0.0001 ETH (in wei)
        )

        val quoteResponse = portal.trading.lifi.getQuote(quoteRequest)

        var txHash: String? = null

        quoteResponse.onSuccess { response ->
            val quote = response.data?.rawResponse
            val transactionRequest = quote?.transactionRequest

            if (transactionRequest == null) {
                Log.e("Portal", "No quote available")
                return@onSuccess
            }

            // 3. Approve the fromToken if it's an ERC-20 (no-op for native assets)
            approveErc20IfNeeded(
                quote = response,
                fromAmount = quoteRequest.fromAmount,
                fromChainId = quoteRequest.fromChain
            )

            // 4. Extract transaction parameters
            if (!transactionRequest.isJsonObject) {
                Log.e("Portal", "Invalid transaction parameters")
                return@onSuccess
            }

            val txParamsJson = transactionRequest.asJsonObject
            val from = txParamsJson.get("from")?.asString
            val to = txParamsJson.get("to")?.asString

            if (from == null || to == null) {
                Log.e("Portal", "Missing required fields")
                return@onSuccess
            }

            var value = "0x0"
            txParamsJson.get("value")?.let { valueElement ->
                value = when {
                    valueElement.isJsonPrimitive && valueElement.asJsonPrimitive.isString -> valueElement.asString
                    valueElement.isJsonPrimitive && valueElement.asJsonPrimitive.isNumber -> {
                        String.format("0x%x", valueElement.asLong)
                    }
                    else -> "0x0"
                }
            }

            val data = txParamsJson.get("data")?.asString ?: ""

            // 5. Sign and submit the transaction
            val ethTransaction = EthTransactionParam(
                from = from,
                to = to,
                value = value,
                data = data,
                gas = null,
                gasPrice = null,
                maxFeePerGas = null,
                maxPriorityFeePerGas = null
            )

            val sendResponse = portal.request(
                chainId = quoteRequest.fromChain,
                method = PortalRequestMethod.eth_sendTransaction,
                params = listOf(ethTransaction),
                options = RequestOptions(signatureApprovalMemo = "Bridge & swap via Li.Fi") // Optional signature approval memo to use for the request
            )

            txHash = sendResponse.result as? String

            if (txHash == null) {
                Log.e("Portal", "Failed to submit transaction")
                return@onSuccess
            }

            Log.i("Portal", "Transaction submitted: $txHash")
        }.onFailure { error ->
            Log.e("Portal", "Error: ${error.message}")
            return@launch
        }

        // 6. Track status for cross-chain completion
        if (txHash != null) {
            val completed = pollForCompletion(
                txHash = txHash,
                fromChain = quoteRequest.fromChain
            )

            if (completed) {
                Log.i("Portal", "Bridge completed successfully!")
            } else {
                Log.e("Portal", "Bridge failed or timed out")
            }
        }
    } catch (error: Throwable) {
        Log.e("Portal", "Error: ${error.message}")
    }
}
```

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