Skip to main content
Portal’s Flutter 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.
import 'package:portal_flutter/portal_flutter.dart';
import 'dart:convert';

try {
  final userAddress = await portal.getAddress('eip155:1');

  final request = LifiQuoteRequest(
    fromChainId: 'eip155:8453', // Base Mainnet
    toChainId: 'eip155:42161', // Arbitrum
    fromTokenAddress: 'ETH',
    toTokenAddress: 'USDC',
    fromAddress: userAddress,
    fromAmount: '100000000000000', // 0.0001 ETH (in wei)
  );

  final response = await portal.trading.lifi.getQuote(request);

  final route = response.route;
  if (route != null) {
    // Process quote response
    final steps = route.steps;
    for (final step in steps) {
      if (step?.estimate != null) {
        print('From amount: ${step!.estimate!.fromAmount}');
        print('To amount: ${step.estimate!.toAmount}');
        print('Execution duration: ${step.estimate!.executionDuration}s');
      }

      // Sign and submit the transaction if transactionRequest is available
      if (step?.transactionRequest != null) {
        await executeTransaction(step!.transactionRequest!, request.fromChainId);
      }
    }
  }
} on PortalException catch (e) {
  print('Error getting quote: ${e.message}');
}
The response includes a route with steps, each containing a transactionRequest JSON string 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.
import 'package:portal_flutter/portal_flutter.dart';

try {
  final userAddress = await portal.getAddress('eip155:1');

  final request = LifiRoutesRequest(
    fromChainId: 'eip155:8453', // Base Mainnet
    fromAmount: '100000000000000', // 0.0001 ETH (in wei)
    fromTokenAddress: 'ETH',
    toChainId: 'eip155:42161', // Arbitrum
    toTokenAddress: 'USDC',
    fromAddress: userAddress,
  );

  final response = await portal.trading.lifi.getRoutes(request);

  final routes = response.routes;

  // Find recommended route
  final recommendedRoute = routes.firstWhere(
    (route) => route?.tags.contains('RECOMMENDED') ?? false,
    orElse: () => routes.isNotEmpty ? routes.first : null,
  );

  if (recommendedRoute != null) {
    print('Selected route: ${recommendedRoute.id}');
    print('Steps: ${recommendedRoute.steps.length}');
    print('From: ${recommendedRoute.fromAmountUSD} USD');
    print('To: ${recommendedRoute.toAmountUSD} USD');

    // Process route steps
    await processRouteSteps(recommendedRoute.steps, request.fromChainId);
  }
} on PortalException catch (e) {
  print('Error getting routes: ${e.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).
Future<LifiStepTransactionResponse?> getStepTransactionDetails(LifiStep step) async {
  try {
    final request = LifiStepTransactionRequest(step: step);
    final response = await portal.trading.lifi.getRouteStep(request);
    return response;
  } on PortalException catch (e) {
    print('Error getting step details: ${e.message}');
    return null;
  }
}
The response includes a transactionRequest JSON string 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 JSON string and sign the transaction. Parse the JSON and extract the to, value, and data fields to sign and submit the transaction.

Signing and Submitting Transactions

import 'dart:convert';

Future<String?> executeTransaction(String transactionRequestJson, String chainId) async {
  try {
    // Parse the transactionRequest JSON string
    final txParams = jsonDecode(transactionRequestJson) as Map<String, dynamic>;

    final to = txParams['to'] as String;
    final data = txParams['data'] as String?;

    // Extract value (default to 0x0 if not present)
    var value = '0x0';
    if (txParams['value'] is String) {
      value = txParams['value'] as String;
    } else if (txParams['value'] is int) {
      value = '0x${(txParams['value'] as int).toRadixString(16)}';
    }

    // Sign and send
    final txHash = await portal.sendTransaction(
      chainId: chainId,
      to: to,
      data: data,
      value: value,
    );

    print('Transaction submitted: $txHash');

    // Wait for on-chain confirmation
    final confirmed = await waitForConfirmation(txHash, chainId);
    if (confirmed) {
      print('Transaction confirmed');
    }

    return txHash;
  } on PortalException catch (e) {
    print('Error executing transaction: ${e.message}');
    return null;
  }
}
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:
Future<bool> processRouteSteps(List<LifiStep?> steps, String fromChainId) async {
  for (var index = 0; index < steps.length; index++) {
    final step = steps[index];
    if (step == null) continue;

    print('Processing step ${index + 1}/${steps.length}: ${step.tool}');

    // 1. Get transaction details for this step
    final stepResponse = await getStepTransactionDetails(step);
    final transactionRequest = stepResponse?.transactionRequest;

    if (transactionRequest == null) {
      print('Failed to get transaction details for step ${index + 1}');
      return false;
    }

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

    print('Step ${index + 1} completed');
  }

  return true;
}

Waiting for Transaction Confirmation

Future<bool> waitForConfirmation(
  String txHash,
  String chainId, {
  int maxAttempts = 30,
  Duration delay = const Duration(seconds: 2),
}) async {
  for (var i = 0; i < maxAttempts; i++) {
    await Future.delayed(delay);

    try {
      final response = await portal.request(
        chainId: chainId,
        method: 'eth_getTransactionReceipt',
        params: [txHash],
      );

      if (response.result != null) {
        final receipt = jsonDecode(response.result!) as Map<String, dynamic>;
        final status = receipt['status'] as String?;
        if (status == '0x1') {
          return true; // Transaction succeeded
        } else if (status == '0x0') {
          return false; // Transaction reverted
        }
      }
    } catch (e) {
      continue;
    }
  }

  return false; // Timeout
}

Tracking Transaction Status

Use the getStatus method to track the status of your cross-chain transfer.
Future<void> trackLiFiStatus(String txHash, String fromChain) async {
  try {
    final request = LifiStatusRequest(
      txHash: txHash,
      fromChain: fromChain,
    );

    final response = await portal.trading.lifi.getStatus(request);

    print('Status: ${response.status}');

    if (response.transactionId != null) {
      print('Transaction ID: ${response.transactionId}');
    }

    if (response.lifiExplorerLink != null) {
      print('Explorer: ${response.lifiExplorerLink}');
    }

    // Check if complete
    switch (response.status) {
      case LifiTransferStatus.done:
        print('Transfer completed successfully!');
        break;
      case LifiTransferStatus.failed:
        print('Transfer failed');
        break;
      default:
        print('Transfer in progress...');
    }
  } on PortalException catch (e) {
    print('Error getting status: ${e.message}');
  }
}

Polling for Cross-Chain Completion

For cross-chain transfers, poll the status endpoint until the transfer completes:
Future<bool> pollForCompletion(
  String txHash,
  String fromChain, {
  int maxAttempts = 300,
  Duration pollInterval = const Duration(seconds: 2),
}) async {
  for (var attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      final request = LifiStatusRequest(
        txHash: txHash,
        fromChain: fromChain,
      );

      final response = await portal.trading.lifi.getStatus(request);

      print('Polling (${attempt + 1}/$maxAttempts): ${response.status}');

      switch (response.status) {
        case LifiTransferStatus.done:
          return true;
        case LifiTransferStatus.failed:
          return false;
        default:
          break; // Continue polling
      }
    } catch (e) {
      // Continue polling on error
    }

    await Future.delayed(pollInterval);
  }

  return false; // Timeout
}

Example Flow

Here’s a complete example of executing a cross-chain bridge:
import 'package:portal_flutter/portal_flutter.dart';
import 'dart:convert';

try {
  // 1. Get user address
  final userAddress = await portal.getAddress('eip155:1');

  // 2. Get a quote
  final quoteRequest = LifiQuoteRequest(
    fromChainId: 'eip155:8453', // Base Mainnet
    toChainId: 'eip155:42161', // Arbitrum
    fromTokenAddress: 'ETH',
    toTokenAddress: 'USDC',
    fromAddress: userAddress,
    fromAmount: '100000000000000', // 0.0001 ETH (in wei)
  );

  final quoteResponse = await portal.trading.lifi.getQuote(quoteRequest);

  final route = quoteResponse.route;
  if (route == null || route.steps.isEmpty) {
    print('No quote available');
    return;
  }

  // 3. Get the first step's transaction request
  final firstStep = route.steps.first;
  String? transactionRequestJson = firstStep?.transactionRequest;

  // If transactionRequest is not directly available, fetch step details
  if (transactionRequestJson == null && firstStep != null) {
    final stepRequest = LifiStepTransactionRequest(step: firstStep);
    final stepResponse = await portal.trading.lifi.getRouteStep(stepRequest);
    transactionRequestJson = stepResponse.transactionRequest;
  }

  if (transactionRequestJson == null) {
    print('No transaction request available');
    return;
  }

  // 4. Parse and execute the transaction
  final txParams = jsonDecode(transactionRequestJson) as Map<String, dynamic>;

  final to = txParams['to'] as String;
  final data = txParams['data'] as String?;

  var value = '0x0';
  if (txParams['value'] is String) {
    value = txParams['value'] as String;
  } else if (txParams['value'] is int) {
    value = '0x${(txParams['value'] as int).toRadixString(16)}';
  }

  final txHash = await portal.sendTransaction(
    chainId: quoteRequest.fromChainId,
    to: to,
    data: data,
    value: value,
  );

  print('Transaction submitted: $txHash');

  // 5. Track status for cross-chain completion
  final completed = await pollForCompletion(
    txHash,
    quoteRequest.fromChainId,
  );

  if (completed) {
    print('Bridge completed successfully!');
  } else {
    print('Bridge failed or timed out');
  }
} on PortalException catch (e) {
  print('Error: ${e.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

Li.Fi supports a wide range of networks for bridging and swapping. Common networks include:
  • Monad (eip155:143)
  • Ethereum (eip155:1)
  • Polygon (eip155:137)
  • Base (eip155:8453)
  • Arbitrum (eip155:42161)
  • Optimism (eip155:10)
  • Avalanche (eip155:43114)
  • And many more…
For the complete list of supported networks, refer to the Li.Fi documentation.

Next Steps