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

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

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

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

            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)
