What We’re Building

A React (Next.js) app that connects Dynamic’s MPC wallets to LiFi’s cross-chain bridge aggregator, allowing users to:
  • Swap tokens across different blockchain networks
  • Find optimal routes with best rates and fees
  • Execute cross-chain transactions seamlessly
  • Monitor transaction progress across chains
If you want to take a quick look at the final code, check out the GitHub repository.

Building the Application

Project Setup

Start by creating a new Dynamic project with React, Viem, Wagmi, and multi-chain support:
npx create-dynamic-app@latest lifi-dynamic-app --framework nextjs --library viem --wagmi true --chains ethereum --pm npm
  
cd lifi-dynamic-app

Install LiFi Dependencies

Add the LiFi SDK:
npm install @lifi/sdk @lifi/wallet-management 
This installs the LiFi SDK for cross-chain routing and execution capabilities.

Configure Environment

Create a .env.local file with your Dynamic environment ID:
.env.local
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your-environment-id-here
NEXT_PUBLIC_LIFI_API_KEY=your-lifi-api-key-here
Set your Dynamic environment ID and LiFi API key for the integration.

Get LiFi API Key with Improved Rate Limits

To get an API key with improved rate limits, you need to create an integration on the LiFi Partner Portal.

Enable Transaction Simulation

  1. Go to your Dynamic dashboard
  2. Navigate to Developer SettingsEmbedded WalletsDynamic
  3. Enable “Show Confirmation UI” and “Transaction Simulation” toggles
This enables transaction previews showing asset transfers, fees, and contract addresses before execution.

Create LiFi Client

Create src/lib/lifi.ts to configure the LiFi SDK:
src/lib/lifi.ts
import { ChainType, EVM, createConfig, getChains } from "@lifi/sdk";
import { getWalletClient, switchChain } from "@wagmi/core";
import type { Config } from "wagmi";

export const initializeLiFiConfig = (wagmiConfig: Config) => {
  return createConfig({
    integrator: "Dynamic",
    providers: [
      EVM({
        getWalletClient: () => {
          const client = getWalletClient(wagmiConfig);
          return client;
        },
        switchChain: async (chainId) => {
          try {
            const chain = await switchChain(wagmiConfig, { chainId });
            const client = getWalletClient(wagmiConfig, { chainId: chain.id });
            return client;
          } catch (error) {
            throw error;
          }
        },
      }),
    ],
    apiKey: process.env.NEXT_PUBLIC_LIFI_API_KEY,
  });
};

export const loadLiFiChains = async () => {
  try {
    const chains = await getChains({
      chainTypes: [ChainType.EVM],
    });
    return chains;
  } catch {
    return [];
  }
};
Here, we’ve created a loadLiFiChains function that fetches EVM chains from LiFi. The other function initializeLiFiConfig is used to initialize the LiFi SDK with wagmi, whose state is managed by Dynamic.

Create LiFi Provider

Create src/lib/lifi-provider.tsx to wrap the LiFi configuration:
src/lib/lifi-provider.tsx
"use client";

import { config as lifiConfig } from "@lifi/sdk";
import { useSyncWagmiConfig } from "@lifi/wallet-management";
import { useQuery } from "@tanstack/react-query";
import { type FC, type PropsWithChildren, useEffect, useState } from "react";
import type { Config, CreateConnectorFn } from "wagmi";
import { initializeLiFiConfig, loadLiFiChains } from "./lifi";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";

interface LiFiProviderProps extends PropsWithChildren {
  wagmiConfig: Config;
  connectors: CreateConnectorFn[];
}

export const LiFiProvider: FC<LiFiProviderProps> = ({
  children,
  wagmiConfig,
  connectors,
}) => {
  const { sdkHasLoaded } = useDynamicContext();
  const [isInitialized, setIsInitialized] = useState(false);

  const {
    data: chains,
    error: chainsError,
    isLoading: chainsLoading,
  } = useQuery({
    queryKey: ["lifi-chains"] as const,
    queryFn: async () => {
      const chains = await loadLiFiChains();

      if (chains.length > 0) {
        lifiConfig.setChains(chains);
      }
      return chains;
    },
    staleTime: 5 * 60 * 1000,
    gcTime: 10 * 60 * 1000,
    retry: 3,
    retryDelay: 1000,
    enabled: sdkHasLoaded,
  });

  useEffect(() => {
    if (sdkHasLoaded && !isInitialized) {
      try {
        initializeLiFiConfig(wagmiConfig);
        setIsInitialized(true);
      } catch {
        setIsInitialized(false);
      }
    }
  }, [sdkHasLoaded, wagmiConfig, isInitialized]);

  useSyncWagmiConfig(wagmiConfig, connectors, chains);

  if (chainsLoading || !sdkHasLoaded || !isInitialized) {
    return (
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          height: "100px",
          fontSize: "14px",
          opacity: 0.7,
        }}
      >
        {!sdkHasLoaded
          ? "Loading Dynamic SDK..."
          : chainsLoading
          ? "Loading LiFi chains..."
          : "Initializing LiFi..."}
      </div>
    );
  }

  if (chainsError) {
    return (
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          height: "100px",
          fontSize: "14px",
          color: "#ef4444",
        }}
      >
        Failed to load LiFi chains. Please refresh the page.
      </div>
    );
  }

  return <>{children}</>;
};
Here, we have created a LiFiProvider component that can get the chains from LiFi and initialize the LiFi SDK with wagmi. We also call the useSyncWagmiConfig function to sync the wagmi config with the LiFi SDK.

Configure Providers

Update src/lib/providers.tsx to include the LiFi configuration:
src/lib/providers.tsx
"use client";

import { LiFiProvider } from "@/lib/lifi-provider";
import { config } from "@/lib/wagmi";
import { EthereumWalletConnectors } from "@dynamic-labs/ethereum";
import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core";
import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import type { CreateConnectorFn } from "wagmi";

export default function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = new QueryClient();
  const connectors: CreateConnectorFn[] = [];

  return (
    <DynamicContextProvider
      theme="auto"
      settings={{
        environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!,
        walletConnectors: [EthereumWalletConnectors],
      }}
    >
      <WagmiProvider config={config}>
        <QueryClientProvider client={queryClient}>
          <DynamicWagmiConnector>
            <LiFiProvider wagmiConfig={config} connectors={connectors}>
              {children}
            </LiFiProvider>
          </DynamicWagmiConnector>
        </QueryClientProvider>
      </WagmiProvider>
    </DynamicContextProvider>
  );
}
This sets up Dynamic wallet providers with LiFi integration and React Query for state management.

Create Multi-Chain Swap Component

Create src/components/MultiChainSwap.tsx for the main swap interface:
src/components/MultiChainSwap.tsx
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import {
  executeRoute,
  getChains,
  getRoutes,
  getTokens,
  type Route,
  type Token,
} from "@lifi/sdk";
import { useEffect, useState } from "react";
import { formatUnits, parseUnits } from "viem";

interface SimpleChain {
  id: number;
  name: string;
}

interface SwapState {
  fromChain: SimpleChain | null;
  toChain: SimpleChain | null;
  fromToken: Token | null;
  toToken: Token | null;
  amount: string;
  routes: Route[];
  selectedRoute: Route | null;
  isLoading: boolean;
  error: string | null;
  txHash: string | null;
}

export default function MultiChainSwap() {
  const { primaryWallet, sdkHasLoaded } = useDynamicContext();
  const isConnected = !!primaryWallet;
  const address = primaryWallet?.address;
  const isReady = sdkHasLoaded && isConnected && !!address;

  const [swapState, setSwapState] = useState<SwapState>({
    fromChain: null,
    toChain: null,
    fromToken: null,
    toToken: null,
    amount: "0.001",
    routes: [],
    selectedRoute: null,
    isLoading: false,
    error: null,
    txHash: null,
  });

  const [chains, setChains] = useState<SimpleChain[]>([]);
  const [fromTokens, setFromTokens] = useState<Token[]>([]);
  const [toTokens, setToTokens] = useState<Token[]>([]);

  useEffect(() => {
    if (!isReady) return;

    const fetchChains = async () => {
      try {
        const availableChains = await getChains();
        const simpleChains: SimpleChain[] = availableChains.map((chain) => ({
          id: chain.id,
          name: chain.name,
        }));
        setChains(simpleChains);

        if (simpleChains.length >= 2) {
          setSwapState((prev) => ({
            ...prev,
            fromChain: simpleChains[0],
            toChain: simpleChains[1],
          }));
        }
      } catch {
        setSwapState((prev) => ({
          ...prev,
          error: "Failed to fetch available chains",
        }));
      }
    };

    fetchChains();
  }, [isReady]);

  useEffect(() => {
    if (!swapState.fromChain || !swapState.toChain || !isReady) return;

    const fetchTokens = async () => {
      try {
        const fromChainId = swapState.fromChain?.id;
        const toChainId = swapState.toChain?.id;

        if (!fromChainId || !toChainId) return;

        const [fromTokensResponse, toTokensResponse] = await Promise.all([
          getTokens({ chains: [fromChainId] }),
          getTokens({ chains: [toChainId] }),
        ]);

        const fromTokensList = fromTokensResponse.tokens[fromChainId] || [];
        const toTokensList = toTokensResponse.tokens[toChainId] || [];

        setFromTokens(fromTokensList);
        setToTokens(toTokensList);

        if (fromTokensList.length > 0) {
          setSwapState((prev) => ({ ...prev, fromToken: fromTokensList[0] }));
        }
        if (toTokensList.length > 0) {
          setSwapState((prev) => ({ ...prev, toToken: toTokensList[0] }));
        }
      } catch {
        setSwapState((prev) => ({
          ...prev,
          error: "Failed to fetch available tokens",
        }));
      }
    };

    fetchTokens();
  }, [swapState.fromChain, swapState.toChain, isReady]);

  const getRoutesForSwap = async () => {
    if (
      !isReady ||
      !swapState.fromChain ||
      !swapState.toChain ||
      !swapState.fromToken ||
      !swapState.toToken
    ) {
      throw new Error("Not ready");
    }

    try {
      const amountInWei = parseUnits(
        swapState.amount,
        swapState.fromToken.decimals
      );

      const routes = await getRoutes({
        fromChainId: swapState.fromChain.id,
        toChainId: swapState.toChain.id,
        fromTokenAddress: swapState.fromToken.address,
        toTokenAddress: swapState.toToken.address,
        fromAmount: amountInWei.toString(),
        fromAddress: address!,
        toAddress: address!,
        options: {
          order: "CHEAPEST",
          maxPriceImpact: 0.3,
          slippage: 0.005,
          fee: 0.01, // 1% fee
        },
      });

      return routes;
    } catch (error) {
      throw error;
    }
  };

  const handleGetRoutes = async () => {
    if (!isFormValid) {
      setSwapState((prev) => ({
        ...prev,
        error: "Please fill in all required fields and connect wallet",
      }));
      return;
    }

    setSwapState((prev) => ({
      ...prev,
      isLoading: true,
      error: null,
      routes: [],
      selectedRoute: null,
    }));

    try {
      const routesResult = await getRoutesForSwap();
      const availableRoutes = routesResult.routes || [];

      setSwapState((prev) => ({
        ...prev,
        routes: availableRoutes,
        selectedRoute: availableRoutes[0] || null,
        isLoading: false,
        error: availableRoutes.length === 0 ? "No routes found" : null,
      }));
    } catch (error) {
      setSwapState((prev) => ({
        ...prev,
        error: error instanceof Error ? error.message : "Failed to get routes",
        isLoading: false,
      }));
    }
  };

  const handleExecuteSwap = async () => {
    if (!swapState.selectedRoute || !isConnected) {
      setSwapState((prev) => ({
        ...prev,
        error: "No route selected or wallet not connected",
      }));
      return;
    }

    setSwapState((prev) => ({
      ...prev,
      isLoading: true,
      error: null,
      txHash: null,
    }));

    try {
      const result = await executeRoute(swapState.selectedRoute, {
        updateRouteHook: (updatedRoute) => {
          console.log("Route updated:", updatedRoute);
        },
        updateTransactionRequestHook: async (txRequest) => {
          return txRequest;
        },
        acceptExchangeRateUpdateHook: async (params) => {
          const accepted = window.confirm(
            `Exchange rate has changed!\nOld amount: ${formatUnits(
              BigInt(params.oldToAmount),
              params.toToken.decimals
            )} ${params.toToken.symbol}\nNew amount: ${formatUnits(
              BigInt(params.newToAmount),
              params.toToken.decimals
            )} ${params.toToken.symbol}\n\nDo you want to continue?`
          );
          return accepted;
        },
        switchChainHook: async (chainId) => {
          try {
            if (primaryWallet?.connector.supportsNetworkSwitching()) {
              await primaryWallet.switchNetwork(chainId);
            }
            return undefined;
          } catch (error) {
            throw error;
          }
        },
        executeInBackground: false,
        disableMessageSigning: false,
      });

      setSwapState((prev) => ({
        ...prev,
        isLoading: false,
        txHash: "Transaction executed successfully",
      }));
    } catch (error) {
      setSwapState((prev) => ({
        ...prev,
        error:
          error instanceof Error ? error.message : "Failed to execute swap",
        isLoading: false,
      }));
    }
  };

  const isFormValid = !!(
    swapState.fromChain &&
    swapState.toChain &&
    swapState.fromToken &&
    swapState.toToken &&
    swapState.amount &&
    isConnected
  );

  if (!isReady) {
    return (
      <div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50 p-6">
        <div className="max-w-6xl mx-auto">
          <div className="text-center py-20">
            <div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center mx-auto mb-4">
              <div className="w-8 h-8 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
            </div>
            <p className="text-xl text-gray-600">
              Loading wallet connection...
            </p>
          </div>
        </div>
      </div>
    );
  }

  if (!isConnected) {
    return (
      <div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50 p-6">
        <div className="max-w-6xl mx-auto">
          <div className="text-center py-20">
            <h2 className="text-2xl font-bold text-gray-900 mb-4">
              Connect Your Wallet
            </h2>
            <p className="text-gray-600 mb-8 max-w-md mx-auto">
              Please connect your wallet to use the multi-chain swap feature.
            </p>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="max-w-4xl mx-auto p-6 space-y-6">
      <h1 className="text-3xl font-bold text-center mb-8">Cross-Chain Swap</h1>

      <div className="bg-white p-6 rounded-lg shadow-md space-y-4">
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-2">
              From Chain
            </label>
            <select
              value={swapState.fromChain?.id || ""}
              onChange={(e) => {
                const chain = chains.find(
                  (c) => c.id === Number(e.target.value)
                );
                setSwapState((prev) => ({
                  ...prev,
                  fromChain: chain || null,
                  fromToken: null,
                }));
              }}
              className="w-full p-2 border border-gray-300 rounded"
            >
              <option value="">Select chain</option>
              {chains.map((chain) => (
                <option key={chain.id} value={chain.id}>
                  {chain.name}
                </option>
              ))}
            </select>
          </div>

          <div>
            <label className="block text-sm font-medium text-gray-700 mb-2">
              To Chain
            </label>
            <select
              value={swapState.toChain?.id || ""}
              onChange={(e) => {
                const chain = chains.find(
                  (c) => c.id === Number(e.target.value)
                );
                setSwapState((prev) => ({
                  ...prev,
                  toChain: chain || null,
                  toToken: null,
                }));
              }}
              className="w-full p-2 border border-gray-300 rounded"
            >
              <option value="">Select chain</option>
              {chains.map((chain) => (
                <option key={chain.id} value={chain.id}>
                  {chain.name}
                </option>
              ))}
            </select>
          </div>
        </div>

        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-2">
              From Token
            </label>
            <select
              value={swapState.fromToken?.address || ""}
              onChange={(e) => {
                const token = fromTokens.find(
                  (t) => t.address === e.target.value
                );
                setSwapState((prev) => ({ ...prev, fromToken: token || null }));
              }}
              className="w-full p-2 border border-gray-300 rounded"
            >
              <option value="">Select token</option>
              {fromTokens.map((token) => (
                <option key={token.address} value={token.address}>
                  {token.symbol} - {token.name}
                </option>
              ))}
            </select>
          </div>

          <div>
            <label className="block text-sm font-medium text-gray-700 mb-2">
              To Token
            </label>
            <select
              value={swapState.toToken?.address || ""}
              onChange={(e) => {
                const token = toTokens.find(
                  (t) => t.address === e.target.value
                );
                setSwapState((prev) => ({ ...prev, toToken: token || null }));
              }}
              className="w-full p-2 border border-gray-300 rounded"
            >
              <option value="">Select token</option>
              {toTokens.map((token) => (
                <option key={token.address} value={token.address}>
                  {token.symbol} - {token.name}
                </option>
              ))}
            </select>
          </div>
        </div>

        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Amount
          </label>
          <input
            type="number"
            value={swapState.amount}
            onChange={(e) =>
              setSwapState((prev) => ({ ...prev, amount: e.target.value }))
            }
            placeholder="Enter amount"
            className="w-full p-2 border border-gray-300 rounded"
          />
        </div>

        <div className="flex gap-4">
          <button
            onClick={handleGetRoutes}
            disabled={!isFormValid || swapState.isLoading}
            className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white py-2 px-4 rounded"
          >
            {swapState.isLoading ? "Loading..." : "Find Routes"}
          </button>

          {swapState.routes.length > 0 && (
            <button
              onClick={handleExecuteSwap}
              disabled={!swapState.selectedRoute || swapState.isLoading}
              className="flex-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white py-2 px-4 rounded"
            >
              Execute Swap
            </button>
          )}
        </div>
      </div>

      {swapState.error && (
        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
          {swapState.error}
        </div>
      )}

      {swapState.txHash && (
        <div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded">
          {swapState.txHash}
        </div>
      )}

      {swapState.routes.length > 0 && (
        <div className="bg-white p-6 rounded-lg shadow-md">
          <h3 className="text-lg font-semibold mb-4">Available Routes</h3>
          <div className="space-y-3">
            {swapState.routes.map((route, index) => (
              <div
                key={index}
                className={`p-3 border rounded cursor-pointer ${
                  swapState.selectedRoute === route
                    ? "border-blue-500 bg-blue-50"
                    : "border-gray-200 hover:border-gray-300"
                }`}
                onClick={() =>
                  setSwapState((prev) => ({ ...prev, selectedRoute: route }))
                }
              >
                <div className="flex justify-between items-center">
                  <span className="font-medium">
                    Route {index + 1} - {route.steps.length} steps
                  </span>
                  <span className="text-sm text-gray-600">
                    {route.toAmount
                      ? `${formatUnits(
                          BigInt(route.toAmount),
                          swapState.toToken?.decimals || 18
                        )} ${swapState.toToken?.symbol}`
                      : "Calculating..."}
                  </span>
                </div>
                <div className="text-sm text-gray-500 mt-1">
                  Estimated time: {route.estimatedTime}s
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}
We’ve created several UI components that you can check out in the GitHub repository. All the functionality is in the MultiChainSwap component. Let’s break it down and see how it works.

How It Works

Data Flow

  1. Chains: Gets available networks from LiFi when the component loads
  2. Tokens: Fetches tokens for selected chains (updates when chains change)
  3. Routes: Finds swap paths when user selects tokens and enters amount
  4. Execution: Runs the swap using the selected route

Key Parameters You Can Customize

Swap Options (in getRoutes):
options: {
  order: "CHEAPEST",        // "CHEAPEST", "FASTEST", "RECOMMENDED"
  maxPriceImpact: 0.3,      // Maximum price impact (30%)
  slippage: 0.005,          // Slippage tolerance (0.5%)
  fee: 0.01                 // Fee percentage (1%)
}
Execution Hooks (in executeRoute):
  • slippage: How much price can change before failing
  • maxPriceImpact: Maximum acceptable price impact
  • fee: Your platform fee percentage
We use useDynamicContext() for wallet management, switchNetwork() for chain switching, manage state updates through a single state object, and provide comprehensive error handling throughout the swap flow.

Collecting Fees

We can collect fees from the user by adding a fee to the getRoutes function.
const routes = await getRoutes({
  fromChainId: swapState.fromChain.id,
  toChainId: swapState.toChain.id,
  fromTokenAddress: swapState.fromToken.address,
  toTokenAddress: swapState.toToken.address,
  fromAmount: amountInWei.toString(),
  fromAddress: address!,
  toAddress: address!,
  options: {
    order: "CHEAPEST",
    maxPriceImpact: 0.3,
    slippage: 0.005,
    fee: 0.01, // 1% fee
  },
});
You can set up wallet addresses where you want to collect fees in the portal dashboard. Configure wallets for fees on LiFi

Run the Application

Start the development server:
npm run dev 
This starts the development server at http://localhost:3000.

Configure CORS

Add your local development URL to the CORS origins in your Dynamic dashboard under Developer SettingsCORS Origins.

Conclusion

This integration demonstrates Dynamic’s MPC wallets connecting to LiFi’s cross-chain bridge aggregator for seamless multi-chain token swaps. The implementation provides comprehensive route management, advanced state handling, and modular architecture for production-ready cross-chain DeFi applications.

Additional Resources