> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Cross-chain swaps with LI.FI

> Learn how to integrate LI.FI's cross-chain bridge aggregator with Dynamic's JavaScript SDK embedded wallets for seamless multi-chain token swaps

## What We're Building

A Next.js app that connects Dynamic's **JavaScript SDK** embedded wallets to LI.FI'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](https://github.com/dynamic-labs/examples/tree/main/examples/nextjs-bridge-swaps-lifi).

[Aave V3 Integration Demo](https://jumpshare.com/embed/3RvO5LRtvUvW7obkiB38)

## Building the Application

### Project setup

Follow the [JavaScript Quickstart](https://docs.dynamic.xyz/docs/javascript/reference/quickstart.md) and use the **Custom setup** path: choose **EVM**. This recipe's example uses **Next.js**—scaffold with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app) and apply the same packages and provider wiring as in the quickstart, or mirror the layout in the [GitHub repository](https://github.com/dynamic-labs/examples/tree/main/examples/nextjs-bridge-swaps-lifi) linked above.

> [!NOTE]
>
> **Console:** Under **Chains & Networks**, enable **every EVM network** your LI.FI routes use (not only Ethereum mainnet). Under **Sign-in Methods** and **Wallets**, enable what your flow needs (including **Embedded wallets**). Under **Security** → **Allowed Origins**, add the origin where the app runs (for example `http://localhost:3000`).

### Install Dependencies

Install the Dynamic JavaScript SDK packages alongside the LI.FI SDK:

```bash npm  theme={"system"}
npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks @lifi/sdk viem @tanstack/react-query
```

```bash yarn theme={"system"}
yarn add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks @lifi/sdk viem @tanstack/react-query
```

```bash pnpm theme={"system"}
pnpm add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks @lifi/sdk viem @tanstack/react-query
```

```bash bun theme={"system"}
bun add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks @lifi/sdk viem @tanstack/react-query
```

The JavaScript SDK has no wagmi connector, so this recipe doesn't install `wagmi`, `@wagmi/core`, or `@lifi/wallet-management` — LI.FI's EVM provider talks to Dynamic wallets directly through viem, which is covered below.

### Configure Environment

Create a `.env.local` file with your Dynamic environment ID:

```env .env.local theme={"system"}
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 LI.FI API key for the integration.

#### Get LI.FI API Key with Improved Rate Limits

To get an API key with improved rate limits, you need to create an integration on the [LI.FI Partner Portal](https://portal.li.fi/integrations).

### Enable Transaction Simulation (Optional)

1. Go to your Dynamic Console
2. Navigate to **Developer Settings** → **Embedded Wallets** → **Dynamic**
3. Enable **"Show Confirmation UI"** and **"Transaction Simulation"** toggles

This enables transaction previews showing asset transfers, fees, gas costs, and contract addresses before execution.

![](https://mintlify.s3.us-west-1.amazonaws.com/dynamic-docs/images/dashboard/console-enable-transaction-simulation.png)

When enabled, users will see a comprehensive transaction preview like this when swapping tokens:

![](https://mintcdn.com/dynamic-docs/S0I4gBjjMnJYbuz8/images/widget/widget-transaction-simulation.png?fit=max\&auto=format\&n=S0I4gBjjMnJYbuz8\&q=85\&s=8d6b2f7207844bfe2c720d23f048353a)

**Note:** These features are optional. You can disable the toggles if you prefer a more streamlined experience without transaction previews and confirmation UI.

### Initialize Dynamic Client

Create `src/lib/dynamic.ts` to set up the JS SDK client and register the EVM extension:

```typescript src/lib/dynamic.ts theme={"system"}
import { createDynamicClient } from "@dynamic-labs-sdk/client";
import { addEvmExtension } from "@dynamic-labs-sdk/evm";

export const dynamicClient = createDynamicClient({
  environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!,
  metadata: { name: "LI.FI Cross-Chain Swap" },
});

if (typeof window !== "undefined") {
  addEvmExtension();
}
```

### Create LI.FI Client

Create `src/lib/lifi.ts` to configure the LI.FI SDK. `getWalletClient` and `switchChain` call the same JavaScript SDK functions regardless of framework, so this file needs no JavaScript/React split — it's plain JavaScript that both a vanilla app and this Next.js app can use as-is:

```typescript src/lib/lifi.ts theme={"system"}
import { ChainType, EVM, createConfig, getChains } from "@lifi/sdk";
import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem";
import { switchActiveNetwork } from "@dynamic-labs-sdk/client";
import type { EvmWalletAccount } from "@dynamic-labs-sdk/evm";

// The JS SDK has no wagmi connector, so LI.FI's EVM provider reads the active
// wallet through this getter instead of a wagmi config. `LiFiProvider` keeps
// it pointed at the current EVM WalletAccount from Dynamic client state.
export const initializeLiFiConfig = (
  getEvmWalletAccount: () => EvmWalletAccount | null
) => {
  return createConfig({
    integrator: "Dynamic",
    providers: [
      EVM({
        getWalletClient: async () => {
          const walletAccount = getEvmWalletAccount();
          if (!walletAccount) {
            throw new Error("No connected EVM wallet");
          }
          return createWalletClientForWalletAccount({ walletAccount });
        },
        switchChain: async (chainId) => {
          const walletAccount = getEvmWalletAccount();
          if (!walletAccount) {
            throw new Error("No connected EVM wallet");
          }
          try {
            await switchActiveNetwork({
              walletAccount,
              networkId: String(chainId),
            });
            // Recreate the wallet client so it reflects the new active network.
            return createWalletClientForWalletAccount({ walletAccount });
          } 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 LI.FI. The other function `initializeLiFiConfig` initializes the LI.FI SDK's EVM provider so it creates and refreshes viem `WalletClient`s directly from the active Dynamic `WalletAccount` — `createWalletClientForWalletAccount` builds the client, and `switchActiveNetwork` moves the wallet to a new chain before the client is rebuilt. Neither call is React-specific; only the wallet account this function is fed (below) differs by framework.

### Create LI.FI Provider

Create `src/lib/lifi-provider.tsx` to wrap the LI.FI configuration. LI.FI's EVM provider isn't reactive — it calls `getEvmWalletAccount()` fresh each time it needs a wallet, so this provider's job is keeping that getter pointed at the latest wallet account:

> [tab] JavaScript
>
> ```javascript theme={"system"}
> import { getWalletAccounts, onEvent } from "@dynamic-labs-sdk/client";
> import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
>
> let evmWalletAccount = getWalletAccounts().find(isEvmWalletAccount) ?? null;
>
> onEvent({
>   event: "walletAccountsChanged",
>   listener: ({ walletAccounts }) => {
>     evmWalletAccount = walletAccounts.find(isEvmWalletAccount) ?? null;
>   },
> });
>
> // Pass a getter, not the value — LI.FI reads this fresh on every call.
> initializeLiFiConfig(() => evmWalletAccount);
> ```

> [tab] React
>
> ```tsx theme={"system"}
> import { useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
> import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm";
> import { useRef } from "react";
>
> const { data: walletAccounts = [] } = useGetWalletAccounts();
> const evmAccount = walletAccounts.find(isEvmWalletAccount) ?? null;
>
> // initializeLiFiConfig reads the active wallet through this ref (rather than
> // a wagmi config) so LI.FI always sees the latest connected EVM account.
> const evmAccountRef = useRef<EvmWalletAccount | null>(evmAccount);
> evmAccountRef.current = evmAccount;
>
> initializeLiFiConfig(() => evmAccountRef.current);
> ```

The full `LiFiProvider` component below also loads the LI.FI chain list with React Query and gates rendering until both the Dynamic client and LI.FI are ready:

```typescript src/lib/lifi-provider.tsx theme={"system"}
"use client";

import { config as lifiConfig } from "@lifi/sdk";
import { useQuery } from "@tanstack/react-query";
import { type FC, type PropsWithChildren, useEffect, useRef, useState } from "react";
import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { useGetWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks";
import { initializeLiFiConfig, loadLiFiChains } from "./lifi";

export const LiFiProvider: FC<PropsWithChildren> = ({ children }) => {
  const { data: initStatus } = useInitStatus();
  const sdkHasLoaded = initStatus === "finished";

  const { data: walletAccounts = [] } = useGetWalletAccounts();
  const evmAccount = walletAccounts.find(isEvmWalletAccount) ?? null;

  // initializeLiFiConfig reads the active wallet through this ref (rather than
  // a wagmi config) so LI.FI always sees the latest connected EVM account.
  const evmAccountRef = useRef<EvmWalletAccount | null>(evmAccount);
  evmAccountRef.current = evmAccount;

  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(() => evmAccountRef.current);
        setIsInitialized(true);
      } catch {
        setIsInitialized(false);
      }
    }
  }, [sdkHasLoaded, isInitialized]);

  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 LI.FI chains..."
          : "Initializing LI.FI..."}
      </div>
    );
  }

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

  return <>{children}</>;
};
```

Here, we have created a `LiFiProvider` component that gets the chains from LI.FI and initializes the LI.FI SDK once the Dynamic client has finished initializing. There's no wagmi config to sync anymore — LI.FI pulls the active `WalletAccount` from `useGetWalletAccounts` through the ref each time it needs a wallet client.

### Configure Providers

Update `src/lib/providers.tsx` to include the LI.FI configuration:

```typescript src/lib/providers.tsx highlight={3,4,7,15,16,17} theme={"system"}
"use client";

import { LiFiProvider } from "@/lib/lifi-provider";
import { dynamicClient } from "@/lib/dynamic";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DynamicProvider } from "@dynamic-labs-sdk/react-hooks";

const queryClient = new QueryClient();

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <DynamicProvider client={dynamicClient}>
        <LiFiProvider>{children}</LiFiProvider>
      </DynamicProvider>
    </QueryClientProvider>
  );
}
```

This sets up the Dynamic client, React Query, and LI.FI initialization. There's no `WagmiProvider` or `DynamicWagmiConnector` — `DynamicProvider` supplies the client to hooks directly, and `LiFiProvider` wires LI.FI to that client's wallet state.

### Create Multi-Chain Swap Component

Create `src/components/MultiChainSwap.tsx` for the main swap interface:

```typescript src/components/MultiChainSwap.tsx theme={"system"}
import {
  isProgrammaticNetworkSwitchAvailable,
  switchActiveNetwork,
} from "@dynamic-labs-sdk/client";
import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { useGetWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks";
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 { data: initStatus } = useInitStatus();
  const sdkHasLoaded = initStatus === "finished";
  const { data: walletAccounts = [] } = useGetWalletAccounts();
  const evmAccount = walletAccounts.find(isEvmWalletAccount) ?? null;
  const isConnected = !!evmAccount;
  const address = evmAccount?.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 (
              evmAccount &&
              isProgrammaticNetworkSwitchAvailable({ walletAccount: evmAccount })
            ) {
              await switchActiveNetwork({
                walletAccount: evmAccount,
                networkId: String(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](https://github.com/dynamic-labs/examples/tree/main/examples/nextjs-bridge-swaps-lifi). 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 LI.FI 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`):

```typescript theme={"system"}
const routes = await getRoutes({
  // fromChainId, toChainId, fromTokenAddress, toTokenAddress, fromAmount, ...
  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 `useGetWalletAccounts()` and `useInitStatus()` for wallet state, `createWalletClientForWalletAccount()` to hand LI.FI a viem `WalletClient` for the active wallet, `switchActiveNetwork()` for chain switching inside `switchChainHook`, manage state updates through a single state object, and provide comprehensive error handling throughout the swap flow. `createWalletClientForWalletAccount` and `switchActiveNetwork` are plain JavaScript SDK calls — see the JavaScript tab under "Create LI.FI Provider" above for the non-React equivalent of getting the active wallet account that feeds them.

### Collecting Fees

We can collect fees from the user by adding a fee to the `getRoutes` function.

```typescript highlight={13} theme={"system"}
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](https://portal.li.fi/integrations).

![Configure wallets for fees on LI.FI](https://mintcdn.com/dynamic-docs/Mu-tjFQVUvR9RrdN/images/lifi/lifi-fees-wallet.png?fit=max\&auto=format\&n=Mu-tjFQVUvR9RrdN\&q=85\&s=14e714182e551c74fad66ee35f20548d)

### Run the Application

Start the development server:

```bash npm  theme={"system"}
npm run dev 
```

```bash yarn theme={"system"}
yarn dev
```

```bash pnpm theme={"system"}
pnpm dev
```

This starts the development server at `http://localhost:3000`.

### Configure CORS

Add your local development URL to the CORS origins in your Dynamic Console under **Developer Settings** → **CORS Origins**.

## Conclusion

This integration demonstrates Dynamic's JavaScript SDK embedded wallets connecting to LI.FI'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 — all without a wagmi connector, since LI.FI's EVM provider talks to the wallet directly through viem.

### Additional Resources

* [JavaScript Quickstart](https://docs.dynamic.xyz/docs/javascript/reference/quickstart.md)
* [Getting a Viem WalletClient](https://docs.dynamic.xyz/docs/javascript/reference/evm/getting-viem-wallet-client.md)
* [Network switching & validation](https://docs.dynamic.xyz/docs/javascript/building-ui/network-switching.md)
* [LI.FI Documentation](https://docs.li.fi/)
* [Cross chain swaps with Mayan](https://docs.dynamic.xyz/docs/recipes/integrations/swaps/mayan.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.