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

# Cross-chain swaps with Mayan

> Learn how to integrate Mayan's cross-chain bridge with Dynamic's JS SDK embedded wallets for EVM-to-any-chain token swaps

## What We're Building

A cross-chain swap application built with Next.js that integrates Dynamic's **JavaScript SDK** (no React SDK dependency) with Mayan's bridge aggregator. This app enables users to:

* Sign in with email OTP, Google, or an injected wallet via a fully custom auth UI
* Execute token swaps from any EVM network to EVM or non-EVM destinations (Solana, Sui, HyperCore)
* Access competitive exchange rates through Mayan's routing system
* Track swap progress in real-time

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

<Frame>
  <iframe src="https://www.loom.com/embed/26820645d73445848407e5ca5a838fcf" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen width="100%" height="640" />
</Frame>

## Building the Application

### Project setup

This recipe uses Next.js with Dynamic's **JavaScript SDK** (`@dynamic-labs-sdk/client` and `@dynamic-labs-sdk/evm`) plus `@dynamic-labs-sdk/react-hooks` for reactive auth state. All signing and wallet creation uses the headless JS SDK; the react-hooks package only provides `DynamicProvider`, `useUser`, and `useGetWalletAccounts` for managing auth state in React without a full UI SDK dependency.

<Info>
  **Dashboard:** Under **Chains & Networks**, enable every EVM network your Mayan routes use (Ethereum, Polygon, BSC, Avalanche, Arbitrum, Optimism, Base). 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`).
</Info>

### Install dependencies

<CodeGroup>
  ```bash npm theme={"system"}
  npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks @mayanfinance/swap-sdk viem @tanstack/react-query
  ```

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

  ```bash yarn theme={"system"}
  yarn add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks @mayanfinance/swap-sdk viem @tanstack/react-query
  ```
</CodeGroup>

### Configure Environment

```env .env theme={"system"}
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here
```

### 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_ENV_ID!,
  metadata: { name: "Mayan Bridge" },
});

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

### Create Wallet Context

Create `src/lib/providers.tsx`. `DynamicProvider` (from `@dynamic-labs-sdk/react-hooks`) owns reactive auth state; `InnerProviders` reads it via `useUser()` and `useGetWalletAccounts()`. The core signing and wallet creation logic still uses the headless JS SDK:

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

import {
  createContext, useContext, useEffect, useCallback, type ReactNode,
} from "react";
import {
  getWalletAccounts, onEvent, isSignedIn, logout,
  detectOAuthRedirect, completeSocialAuthentication,
} from "@dynamic-labs-sdk/client";
import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas";
import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DynamicProvider, useUser, useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
import { dynamicClient } from "./dynamic";

interface WalletContextValue {
  evmAccount: EvmWalletAccount | null;
  loggedIn: boolean;
  ensureEvmWallet: () => Promise<void>;
  disconnect: () => Promise<void>;
}

const WalletContext = createContext<WalletContextValue>({
  evmAccount: null,
  loggedIn: false,
  ensureEvmWallet: async () => {},
  disconnect: async () => {},
});

export function useWallet() {
  return useContext(WalletContext);
}

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false } },
});

function InnerProviders({ children }: { children: ReactNode }) {
  const { data: user } = useUser();
  const loggedIn = user !== null;
  const { data: walletAccounts = [] } = useGetWalletAccounts();
  const evmAccount = walletAccounts.find(isEvmWalletAccount) ?? null;

  const disconnect = useCallback(async () => {
    await logout(dynamicClient);
  }, []);

  const ensureEvmWallet = useCallback(async () => {
    try {
      const accounts = getWalletAccounts(dynamicClient);
      if (!accounts.some(isEvmWalletAccount) && isSignedIn(dynamicClient)) {
        await createWaasWalletAccounts({ chains: ["EVM"] }, dynamicClient);
      }
    } catch {}
  }, []);

  useEffect(() => {
    const unsub = onEvent(
      { event: "walletAccountsChanged", listener: () => { void ensureEvmWallet(); } },
      dynamicClient,
    );
    return () => unsub?.();
  }, [ensureEvmWallet]);

  useEffect(() => {
    const handleOAuthRedirect = async () => {
      if (typeof window === "undefined") return;
      try {
        const url = new URL(window.location.href);
        if (await detectOAuthRedirect({ url }, dynamicClient)) {
          await completeSocialAuthentication({ url }, dynamicClient);
          await ensureEvmWallet();
          window.history.replaceState({}, "", window.location.pathname);
        }
      } catch {}
    };
    handleOAuthRedirect();
  }, [ensureEvmWallet]);

  return (
    <WalletContext.Provider value={{ evmAccount, loggedIn, ensureEvmWallet, disconnect }}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WalletContext.Provider>
  );
}

export default function Providers({ children }: { children: ReactNode }) {
  return (
    <DynamicProvider client={dynamicClient}>
      <InnerProviders>{children}</InnerProviders>
    </DynamicProvider>
  );
}
```

### Define Supported Chains

Create `src/constants/chains.ts`. The FROM chain must be EVM (the wallet signs the transaction); the TO chain can be any chain Mayan supports:

```typescript src/constants/chains.ts theme={"system"}
import { mainnet, polygon, bsc, avalanche, arbitrum, optimism, base } from "viem/chains";

export type ChainKey =
  | "ethereum" | "polygon" | "bsc" | "avalanche"
  | "arbitrum" | "optimism" | "base"
  | "solana" | "sui" | "hypercore";

export const EVM_CHAINS = [
  { id: mainnet.id,  name: "Ethereum Mainnet", key: "ethereum"  as ChainKey },
  { id: polygon.id,  name: "Polygon",          key: "polygon"   as ChainKey },
  { id: bsc.id,      name: "BSC",              key: "bsc"       as ChainKey },
  { id: avalanche.id,name: "Avalanche",        key: "avalanche" as ChainKey },
  { id: arbitrum.id, name: "Arbitrum",         key: "arbitrum"  as ChainKey },
  { id: optimism.id, name: "Optimism",         key: "optimism"  as ChainKey },
  { id: base.id,     name: "Base",             key: "base"      as ChainKey },
];

export const NON_EVM_CHAINS = [
  { id: "solana",    name: "Solana",                    key: "solana"    as ChainKey },
  { id: "sui",       name: "Sui",                       key: "sui"       as ChainKey },
  { id: "hypercore", name: "HyperCore (Hyperliquid)",   key: "hypercore" as ChainKey },
];

export const ALL_CHAINS = [...EVM_CHAINS, ...NON_EVM_CHAINS];

export const isEVMChain = (chain: { id: number | string }): boolean =>
  typeof chain.id === "number";
```

### Create Multi-Chain Swap Component

Create `src/components/MultiChainSwap.tsx`. The core integration uses `getSwapFromEvmTxPayload` to build the transaction and `createWalletClientForWalletAccount` to send it via the user's EVM wallet:

```typescript src/components/MultiChainSwap.tsx theme={"system"}
"use client";

import { useEffect, useState } from "react";
import {
  createPublicClient, createWalletClient, custom, erc20Abi, http, parseUnits, type Chain,
} from "viem";
import { mainnet, polygon, bsc, avalanche, arbitrum, optimism, base } from "viem/chains";
import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem";
import {
  fetchQuote, getSwapFromEvmTxPayload, getEvmChainIdByName, addresses,
} from "@mayanfinance/swap-sdk";
import type { Quote } from "@mayanfinance/swap-sdk";
import { ALL_CHAINS, EVM_CHAINS, isEVMChain } from "@/constants/chains";
import { fetchTokensForChain } from "@/lib/mayan-api";
import { useWallet } from "@/lib/providers";

const VIEM_CHAINS: Record<string, Chain> = {
  ethereum: mainnet, polygon, bsc, avalanche, arbitrum, optimism, base,
};

export default function MultiChainSwap() {
  const { evmAccount, loggedIn } = useWallet();
  const isConnected = loggedIn && !!evmAccount;
  const address = evmAccount?.address;

  // ... state and token loading (see full example in the GitHub repo)

  const executeSwapQuote = async (quote: Quote) => {
    if (!isConnected || !address || !evmAccount) throw new Error("Not ready");

    const viemChain = VIEM_CHAINS[quote.fromChain];
    if (!viemChain) throw new Error(`Unsupported source chain: ${quote.fromChain}`);

    const chainId = getEvmChainIdByName(quote.fromChain);

    // Get the Dynamic wallet client and rewrap it for the target chain
    const dynamicWalletClient = await createWalletClientForWalletAccount({ walletAccount: evmAccount });

    // Switch wallet to the target chain before sending
    await dynamicWalletClient.request({
      method: "wallet_switchEthereumChain",
      params: [{ chainId: `0x${viemChain.id.toString(16)}` }],
    });

    const walletClient = createWalletClient({
      account: dynamicWalletClient.account,
      chain: viemChain,
      transport: custom({
        request: async ({ method, params }: { method: string; params?: unknown[] }) => {
          if (method === "eth_chainId") return `0x${viemChain.id.toString(16)}`;
          return dynamicWalletClient.request({ method, params } as Parameters<typeof dynamicWalletClient.request>[0]);
        },
      }),
    });

    // Handle ERC-20 approval if needed
    const fromTokenContract = quote.fromToken.contract as `0x${string}`;
    const isNative = !fromTokenContract ||
      fromTokenContract === "0x0000000000000000000000000000000000000000";

    if (!isNative) {
      const publicClient = createPublicClient({ chain: viemChain, transport: http() });
      const forwarder = addresses.MAYAN_FORWARDER_CONTRACT as `0x${string}`;
      const allowance = await publicClient.readContract({
        address: fromTokenContract,
        abi: erc20Abi,
        functionName: "allowance",
        args: [address as `0x${string}`, forwarder],
      });
      const required = BigInt(quote.effectiveAmountIn64);
      if (allowance < required) {
        const approveTx = await walletClient.writeContract({
          address: fromTokenContract, abi: erc20Abi, functionName: "approve",
          args: [forwarder, required],
          account: address as `0x${string}`, chain: viemChain,
        });
        await publicClient.waitForTransactionReceipt({ hash: approveTx });
      }
    }

    // Build and send the swap transaction
    const txPayload = getSwapFromEvmTxPayload(quote, address, address, null, address, chainId, null, null);

    return walletClient.sendTransaction({
      to: txPayload.to as `0x${string}`,
      data: txPayload.data as `0x${string}`,
      value: txPayload.value != null ? BigInt(txPayload.value.toString()) : BigInt(0),
      account: address as `0x${string}`,
      chain: viemChain,
      gas: txPayload.gasLimit != null ? BigInt(txPayload.gasLimit.toString()) : undefined,
    });
  };

  // ... handleGetQuote, handleExecuteSwap, and UI rendering
  // See the complete component in the GitHub repository
}
```

The full component with state management, token loading, and UI rendering is available in the [GitHub repository](https://github.com/dynamic-labs/examples/tree/main/examples/nextjs-bridge-mayan).

## How It Works

### Technical Implementation

#### Authentication (JS SDK + react-hooks for reactive state)

* **`createDynamicClient`** → initializes the headless Dynamic client with your environment ID.
* **`addEvmExtension`** → registers EVM wallet support (called client-side only).
* **`sendEmailOTP` / `verifyOTP`** → email magic-link authentication.
* **`authenticateWithSocial`** → social OAuth (Google, etc.) with redirect handling.
* **`connectAndVerifyWithWalletProvider`** → connect MetaMask or other injected wallets.
* **`createWaasWalletAccounts`** → auto-create an embedded EVM wallet on sign-up.

#### Chain Switching

Before any transaction, `wallet_switchEthereumChain` is called on the Dynamic wallet client to ensure the user's wallet is on the correct network. A custom viem transport is then created that proxies all requests through the Dynamic wallet, with `eth_chainId` overridden to match the target chain.

#### Quote Generation

`fetchQuote` from `@mayanfinance/swap-sdk` accepts the from/to token contracts and chain keys, returning one or more route candidates. The integration uses the first (best) quote.

#### ERC-20 Approval

Before executing a non-native token swap, the integration:

1. Reads the current allowance via `publicClient.readContract`
2. If insufficient, calls `walletClient.writeContract` to approve the **Mayan Forwarder** contract
3. Waits for the approval receipt before proceeding

#### Swap Execution

`getSwapFromEvmTxPayload` builds the EVM transaction payload (no ethers.js signer required). The payload is sent directly via `walletClient.sendTransaction`.

### Supported Chains

| Direction                      | Chains                                                      |
| ------------------------------ | ----------------------------------------------------------- |
| **FROM** (source, must be EVM) | Ethereum, Polygon, BSC, Avalanche, Arbitrum, Optimism, Base |
| **TO** (destination)           | All of the above + Solana, Sui, HyperCore                   |

### Referral Fee Setup

```typescript theme={"system"}
const quote = (await fetchQuote({
  amountIn64: amountInWei.toString(),
  fromToken: fromToken.contract,
  toToken: toToken.contract,
  fromChain: fromChain.key,
  toChain: toChain.key,
  slippageBps: "auto",
  referrerBps: 100, // 1% referral fee
}))[0];

const txPayload = getSwapFromEvmTxPayload(
  quote,
  address,
  address,
  { evm: "0xYourFeeRecipientAddress" }, // referrer addresses
  address,
  chainId,
  null,
  null
);
```

### Run the Application

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

Open [http://localhost:3000](http://localhost:3000).

## Conclusion

This recipe demonstrates how to build a production-grade cross-chain swap interface using Dynamic's **JavaScript SDK** with `@dynamic-labs-sdk/react-hooks` for reactive auth state. All signing, wallet creation, and session management use the headless JS SDK; the react-hooks package adds lightweight `DynamicProvider`/`useUser`/`useGetWalletAccounts` bindings without pulling in a full UI SDK.

### Additional Resources

* [Dynamic JavaScript SDK Documentation](https://docs.dynamic.xyz/javascript)
* [Mayan Finance Documentation](https://docs.mayan.finance)
* [Mayan SDK npm](https://www.npmjs.com/package/@mayanfinance/swap-sdk)
* [Cross-chain swaps with LI.FI](/recipes/integrations/swaps/lifi)
