Skip to main content

Documentation Index

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

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

Portal’s 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)
  • Li.Fi integration enabled in your Portal Dashboard (see Li.Fi Integration)

Getting a Quote

Use the getQuote method to get a quote for bridging or swapping tokens across chains.
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.
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).
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:
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}")
    }
}
This step only applies when the fromToken is an ERC-20. Native-asset swaps (ETH, MATIC, etc.) skip it. For more on the delegations API, see the Manage Token Delegations guide.

Signing and Submitting Transactions

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}")
    }
}
The transactionRequest from Li.Fi may include gasPrice and gasLimit fields. You can remove these if you want Portal to estimate the gas for you, or include them if you want to use Li.Fi’s estimates.

Processing Multiple Route Steps

For routes with multiple steps, process them sequentially:
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

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.
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:
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:
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. If you need a chain that isn’t listed above, contact Portal support.
Testnets are not supported.

Next Steps