Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions examples/nextjs-bridge-mayan/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ A cross-chain swap demo using **Dynamic's JavaScript SDK** (no React SDK depende

## Features

- **Dynamic JS SDK**: Headless auth with email OTP, Google OAuth, and injected EVM wallets via `@dynamic-labs-sdk/client`
- **Embedded EVM Wallets**: WaaS wallets created automatically on sign-up via `@dynamic-labs-sdk/evm`
- **Dynamic JS SDK**: Headless auth with email OTP, Google OAuth, and injected EVM wallets via `@dynamic-labs/client`
- **Embedded EVM Wallets**: WaaS wallets created automatically on sign-up via `@dynamic-labs/evm`
- **Mayan Routing**: Cross-chain quotes and swap execution using `@mayanfinance/swap-sdk`
- **EVM → Any Chain**: Source chain must be EVM; destination supports Solana, Sui, HyperCore, and all EVM chains
- **ERC-20 Approvals**: Automatic allowance check and approval before swap execution
- **Chain Switching**: `wallet_switchEthereumChain` called automatically when the selected FROM chain differs from the wallet's active network

## Tech Stack

| Layer | Library |
|---|---|
| Auth & Wallets | `@dynamic-labs-sdk/client` + `@dynamic-labs-sdk/evm` |
| Chain interaction | `viem` |
| Cross-chain swaps | `@mayanfinance/swap-sdk` |
| UI | Next.js 15, Tailwind CSS |
| Layer | Library |
| ----------------- | -------------------------------------------- |
| Auth & Wallets | `@dynamic-labs/client` + `@dynamic-labs/evm` |
| Chain interaction | `viem` |
| Cross-chain swaps | `@mayanfinance/swap-sdk` |
| UI | Next.js 15, Tailwind CSS |

## Getting Started

Expand Down Expand Up @@ -59,10 +59,10 @@ Open [http://localhost:3000](http://localhost:3000).

## Supported Networks

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

## Learn More

Expand Down
5 changes: 3 additions & 2 deletions examples/nextjs-bridge-mayan/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
"lint": "next lint"
},
"dependencies": {
"@dynamic-labs-sdk/client": "^0.24.1",
"@dynamic-labs-sdk/evm": "^0.24.1",
"@dynamic-labs-sdk/client": "1.2.1",
"@dynamic-labs-sdk/evm": "1.2.1",
"@dynamic-labs-sdk/react-hooks": "0.26.5",
"@mayanfinance/swap-sdk": "10.9.3",
"lucide-react": "0.542.0",
"@tanstack/react-query": "5.85.3",
Expand Down
3,045 changes: 1,981 additions & 1,064 deletions examples/nextjs-bridge-mayan/pnpm-lock.yaml

Large diffs are not rendered by default.

112 changes: 81 additions & 31 deletions examples/nextjs-bridge-mayan/src/components/MultiChainSwap.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
"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 {
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 { ALL_CHAINS, EVM_CHAINS, type ChainKey, isEVMChain } from "@/constants/chains";
import {
ALL_CHAINS,
EVM_CHAINS,
type ChainKey,
isEVMChain,
} from "@/constants/chains";
import { fetchTokensForChain, type TokenData } from "@/lib/mayan-api";
import { useWallet } from "@/lib/providers";
import ActionButtons from "./ActionButtons";
import RouteDisplay from "./RouteDisplay";
import StatusMessages from "./StatusMessages";
import SwapForm from "./SwapForm";
import { fetchQuote, getSwapFromEvmTxPayload, getEvmChainIdByName, addresses } from "@mayanfinance/swap-sdk";
import {
fetchQuote,
getSwapFromEvmTxPayload,
getEvmChainIdByName,
addresses,
} from "@mayanfinance/swap-sdk";
import type { Quote, Token } from "@mayanfinance/swap-sdk";

const VIEM_CHAINS: Record<string, Chain> = {
Expand Down Expand Up @@ -95,10 +121,10 @@ export default function MultiChainSwap() {
]);

const sortedFromTokens = sortTokensByPopularity(
fromTokensResponse.map(convertTokenDataToToken)
fromTokensResponse.map(convertTokenDataToToken),
);
const sortedToTokens = sortTokensByPopularity(
toTokensResponse.map(convertTokenDataToToken)
toTokensResponse.map(convertTokenDataToToken),
);

setFromTokens(sortedFromTokens);
Expand Down Expand Up @@ -127,7 +153,7 @@ export default function MultiChainSwap() {

const loadTokensForChain = async (
chainId: number | string,
isFromChain: boolean
isFromChain: boolean,
) => {
if (typeof chainId !== "number") {
if (isFromChain) {
Expand All @@ -142,7 +168,7 @@ export default function MultiChainSwap() {
try {
const tokens = await fetchTokensForChain(chainId);
const sortedTokens = sortTokensByPopularity(
tokens.map(convertTokenDataToToken)
tokens.map(convertTokenDataToToken),
);

if (isFromChain) {
Expand All @@ -165,15 +191,24 @@ export default function MultiChainSwap() {

const sortTokensByPopularity = (tokens: Token[]): Token[] => {
const popularSymbols = [
"USDC", "USDT", "ETH", "WETH", "WBTC", "DAI", "MATIC", "BNB", "AVAX", "ARB",
"USDC",
"USDT",
"ETH",
"WETH",
"WBTC",
"DAI",
"MATIC",
"BNB",
"AVAX",
"ARB",
];

return tokens.sort((a, b) => {
const aIndex = popularSymbols.findIndex((symbol) =>
a.symbol.toUpperCase().includes(symbol.toUpperCase())
a.symbol.toUpperCase().includes(symbol.toUpperCase()),
);
const bIndex = popularSymbols.findIndex((symbol) =>
b.symbol.toUpperCase().includes(symbol.toUpperCase())
b.symbol.toUpperCase().includes(symbol.toUpperCase()),
);

if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
Expand All @@ -189,32 +224,32 @@ export default function MultiChainSwap() {
}

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

const chainId = getEvmChainIdByName(quote.fromChain);

const dynamicWalletClient = await createWalletClientForWalletAccount({
walletAccount: evmAccount,
});

// Switch the wallet to the target chain before sending any transactions.
// For injected wallets (MetaMask etc.) this triggers the chain-switch prompt.
// For WaaS wallets it updates the active network in Dynamic's context.
await dynamicWalletClient.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: `0x${viemChain.id.toString(16)}` }],
});

// Build a fresh wallet client now that the wallet is on the correct chain.
const walletClient = createWalletClient({
account: dynamicWalletClient.account,
chain: viemChain,
transport: custom({
request: async ({ method, params }: { method: string; params?: unknown[] }) => {
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]);
return dynamicWalletClient.request({ method, params } as Parameters<
typeof dynamicWalletClient.request
>[0]);
},
}),
});
Expand All @@ -225,8 +260,12 @@ export default function MultiChainSwap() {
fromTokenContract === "0x0000000000000000000000000000000000000000";

if (!isNativeToken) {
const publicClient = createPublicClient({ chain: viemChain, transport: http() });
const forwarderAddress = addresses.MAYAN_FORWARDER_CONTRACT as `0x${string}`;
const publicClient = createPublicClient({
chain: viemChain,
transport: http(),
});
const forwarderAddress =
addresses.MAYAN_FORWARDER_CONTRACT as `0x${string}`;
const allowance = await publicClient.readContract({
address: fromTokenContract,
abi: erc20Abi,
Expand Down Expand Up @@ -255,16 +294,22 @@ export default function MultiChainSwap() {
address,
chainId,
null,
null
null,
);

const txHash = await walletClient.sendTransaction({
to: txPayload.to as `0x${string}`,
data: txPayload.data as `0x${string}`,
value: txPayload.value != null ? BigInt(txPayload.value.toString()) : BigInt(0),
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,
gas:
txPayload.gasLimit != null
? BigInt(txPayload.gasLimit.toString())
: undefined,
});

return txHash;
Expand Down Expand Up @@ -296,7 +341,7 @@ export default function MultiChainSwap() {
try {
const amountInWei = parseUnits(
swapState.amount,
swapState.fromToken.decimals
swapState.fromToken.decimals,
);

const fromChain = swapState.fromChain;
Expand Down Expand Up @@ -366,7 +411,8 @@ export default function MultiChainSwap() {
} catch (error) {
setSwapState((prev) => ({
...prev,
error: error instanceof Error ? error.message : "Failed to execute swap",
error:
error instanceof Error ? error.message : "Failed to execute swap",
isLoading: false,
isExecuting: false,
}));
Expand Down Expand Up @@ -404,7 +450,11 @@ export default function MultiChainSwap() {
toTokens={toTokens}
isLoadingTokens={isLoadingTokens}
onFromChainChange={(chain) => {
setSwapState((prev) => ({ ...prev, fromChain: chain, fromToken: null }));
setSwapState((prev) => ({
...prev,
fromChain: chain,
fromToken: null,
}));
if (chain) {
loadTokensForChain(chain.id, true);
}
Expand Down
Loading