Skip to main content

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.

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.

Building the Application

Project setup

This recipe uses Next.js with Dynamic’s JavaScript SDK (@dynamic-labs-sdk/client and @dynamic-labs-sdk/evm). The JS SDK is framework-agnostic — no React-specific Dynamic packages are required.
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 SecurityAllowed Origins, add the origin where the app runs (for example http://localhost:3000).

Install dependencies

npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @mayanfinance/swap-sdk viem @tanstack/react-query

Configure Environment

.env
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:
src/lib/dynamic.ts
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 to expose wallet state to your React tree using standard React context — no Dynamic React SDK required:
src/lib/providers.tsx
"use client";

import {
  createContext, useContext, useState, 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 { 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();

export default function Providers({ children }: { children: ReactNode }) {
  const [evmAccount, setEvmAccount] = useState<EvmWalletAccount | null>(null);
  const [loggedIn, setLoggedIn] = useState(false);

  const refresh = useCallback(() => {
    const accounts = getWalletAccounts(dynamicClient);
    setEvmAccount(accounts.find(isEvmWalletAccount) ?? null);
    setLoggedIn(isSignedIn(dynamicClient));
  }, []);

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

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

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

    const unsub1 = onEvent(
      { event: "walletAccountsChanged", listener: () => ensureEvmWallet() },
      dynamicClient
    );
    const unsub2 = onEvent(
      { event: "logout", listener: () => { setEvmAccount(null); setLoggedIn(false); } },
      dynamicClient
    );
    return () => { unsub1(); unsub2(); };
  }, [refresh, ensureEvmWallet]);

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

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:
src/constants/chains.ts
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:
src/components/MultiChainSwap.tsx
"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.

How It Works

Technical Implementation

Authentication (JS SDK, no React dependency)

  • 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

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

Referral Fee Setup

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

pnpm dev
Open http://localhost:3000.

Conclusion

This recipe demonstrates how to build a production-grade cross-chain swap interface using Dynamic’s JavaScript SDK — no React-specific Dynamic packages required. The JS SDK approach gives you full control over the auth UI while Dynamic handles wallet key management, embedded wallet creation, and session security.

Additional Resources