"use client";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { useEffect, useState } from "react";
import { erc20Abi, maxUint256, parseUnits } from "viem";
import {
useAccount,
useChainId,
usePublicClient,
useSendTransaction,
useWriteContract,
} from "wagmi";
import { EVM_CHAINS } from "@/constants/chains";
import { fetchTokensForChain } from "@/lib/mayan-api";
import type { SwapState } from "@/types/swap";
import type { Quote, Token } from "@mayanfinance/swap-sdk";
import {
addresses,
fetchQuote,
getSwapFromEvmTxPayload,
} from "@mayanfinance/swap-sdk";
import ActionButtons from "./ActionButtons";
import RouteDisplay from "./RouteDisplay";
import StatusMessages from "./StatusMessages";
import SwapForm from "./SwapForm";
import { MayanApiToken } from "@/types/mayan-api";
export default function MultiChainSwap() {
const { sdkHasLoaded } = useDynamicContext();
const { address, isConnected } = useAccount();
const chainId = useChainId();
const { sendTransaction } = useSendTransaction();
const { writeContract } = useWriteContract();
const publicClient = usePublicClient();
const [swapState, setSwapState] = useState<SwapState>({
fromChain: EVM_CHAINS[0],
toChain: EVM_CHAINS[1],
fromToken: null,
toToken: null,
amount: "0.000001",
quote: null,
isLoading: false,
error: null,
txHash: null,
mayanSwapHash: null,
isExecuting: false,
isCheckingAllowance: false,
isApproving: false,
needsApproval: false,
allowanceAmount: BigInt(0),
});
const [fromTokens, setFromTokens] = useState<Token[]>([]);
const [toTokens, setToTokens] = useState<Token[]>([]);
const [isLoadingTokens, setIsLoadingTokens] = useState(false);
const convertTokenDataToToken = (tokenData: MayanApiToken): Token =>
({
contract: tokenData.contract,
symbol: tokenData.symbol,
name: tokenData.name,
decimals: tokenData.decimals,
logoURI: tokenData.logoURI || "",
chainId: tokenData.chainId,
mint: tokenData.contract,
coingeckoId: "",
supportsPermit: false,
verified: true,
standard: "erc20",
} as Token);
useEffect(() => {
const loadTokens = async () => {
if (!swapState.fromChain?.id || !swapState.toChain?.id) return;
setIsLoadingTokens(true);
try {
const fromChainId = swapState.fromChain.id;
const toChainId = swapState.toChain.id;
if (typeof fromChainId !== "number" || typeof toChainId !== "number") {
setFromTokens([]);
setToTokens([]);
return;
}
const [fromTokensResponse, toTokensResponse] = await Promise.all([
fetchTokensForChain(fromChainId),
fetchTokensForChain(toChainId),
]);
setFromTokens(fromTokensResponse.map(convertTokenDataToToken));
setToTokens(toTokensResponse.map(convertTokenDataToToken));
setSwapState((prev) => ({
...prev,
fromToken: null,
toToken: null,
quote: null,
}));
} catch {
setFromTokens([]);
setToTokens([]);
} finally {
setIsLoadingTokens(false);
}
};
loadTokens();
}, [swapState.fromChain?.id, swapState.toChain?.id]);
const loadTokensForChain = async (
chainId: number | string,
isFromChain: boolean
) => {
if (typeof chainId !== "number") {
if (isFromChain) {
setFromTokens([]);
} else {
setToTokens([]);
}
return;
}
setIsLoadingTokens(true);
try {
const tokens = await fetchTokensForChain(chainId);
if (isFromChain) {
setFromTokens(tokens.map(convertTokenDataToToken));
setSwapState((prev) => ({
...prev,
fromToken: null,
quote: null,
}));
} else {
setToTokens(tokens.map(convertTokenDataToToken));
setSwapState((prev) => ({
...prev,
toToken: null,
quote: null,
}));
}
} catch {
if (isFromChain) {
setFromTokens([]);
} else {
setToTokens([]);
}
} finally {
setIsLoadingTokens(false);
}
};
const isNativeToken = (token: Token | null): boolean => {
if (!token) return false;
const nativeAddresses = [
"0x0000000000000000000000000000000000000000",
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"",
];
return token.symbol === "ETH" && nativeAddresses.includes(token.contract);
};
const checkTokenAllowance = async (
token: Token,
amountNeeded: bigint
): Promise<{
hasEnoughAllowance: boolean;
currentAllowance: bigint;
}> => {
if (!address || !token || isNativeToken(token) || !publicClient) {
return { hasEnoughAllowance: true, currentAllowance: BigInt(0) };
}
try {
const currentAllowance = (await publicClient.readContract({
address: token.contract as `0x${string}`,
abi: erc20Abi,
functionName: "allowance",
args: [
address as `0x${string}`,
addresses.MAYAN_FORWARDER_CONTRACT as `0x${string}`,
],
})) as bigint;
return {
hasEnoughAllowance: currentAllowance >= amountNeeded,
currentAllowance,
};
} catch {
throw new Error("Failed to check token allowance");
}
};
const approveToken = async (
token: Token,
amount?: bigint
): Promise<string> => {
if (!address || !token || isNativeToken(token)) {
throw new Error("Invalid token or native token doesn't need approval");
}
const approvalAmount = amount || maxUint256;
return new Promise<string>((resolve, reject) => {
writeContract(
{
address: token.contract as `0x${string}`,
abi: erc20Abi,
functionName: "approve",
args: [
addresses.MAYAN_FORWARDER_CONTRACT as `0x${string}`,
approvalAmount,
],
},
{
onSuccess: (hash) => resolve(hash),
onError: (error) => reject(error),
}
);
});
};
const supportsPermit = (token: Token): boolean => {
return token.supportsPermit || false;
};
const executeSwapQuote = async (
quote: Quote,
permit?: {
value: bigint;
deadline: number;
v: number;
r: string;
s: string;
} | null
) => {
if (!sdkHasLoaded || !isConnected || !address || !quote) {
throw new Error("Not ready");
}
try {
const txPayload = getSwapFromEvmTxPayload(
quote,
address,
address,
{
evm: "0x0000000000000000000000000000000000000000",
},
address,
chainId,
null,
permit,
{}
);
const txHash = await new Promise<string>((resolve, reject) => {
sendTransaction(
{
to: txPayload.to as `0x${string}`,
value: txPayload.value
? BigInt(txPayload.value.toString())
: undefined,
data: txPayload.data as `0x${string}`,
gas: txPayload.gasLimit
? BigInt(txPayload.gasLimit.toString())
: undefined,
gasPrice: txPayload.gasPrice
? BigInt(txPayload.gasPrice.toString())
: undefined,
},
{
onSuccess: (hash) => resolve(hash),
onError: (error) => reject(error),
}
);
});
return txHash;
} catch (error) {
throw error;
}
};
const handleGetQuote = async () => {
if (
!swapState.fromChain ||
!swapState.toChain ||
!swapState.fromToken ||
!swapState.toToken ||
!swapState.amount ||
!isConnected
) {
setSwapState((prev) => ({
...prev,
error: "Please fill in all required fields and connect wallet",
}));
return;
}
setSwapState((prev) => ({
...prev,
isLoading: true,
error: null,
quote: null,
}));
try {
const amountInWei = parseUnits(
swapState.amount,
swapState.fromToken.decimals
);
const quote = (
await fetchQuote({
amountIn64: amountInWei.toString(),
fromToken: swapState.fromToken.contract,
toToken: swapState.toToken.contract,
fromChain: swapState.fromChain.key,
toChain: swapState.toChain.key,
slippageBps: "auto",
referrerBps: 100,
})
)[0];
if (!quote) {
throw new Error("No quote available for this swap");
}
setSwapState((prev) => ({
...prev,
quote,
isLoading: false,
error: null,
}));
} catch (error) {
setSwapState((prev) => ({
...prev,
error: (error as Error).message || "Failed to get quote",
isLoading: false,
}));
}
};
const handleExecuteSwap = async () => {
if (!swapState.quote || !isConnected || !swapState.fromToken) {
setSwapState((prev) => ({
...prev,
error:
"No quote available, wallet not connected, or token not selected",
}));
return;
}
setSwapState((prev) => ({
...prev,
isLoading: true,
error: null,
txHash: null,
mayanSwapHash: null,
isExecuting: true,
isCheckingAllowance: true,
}));
try {
const amountNeeded = parseUnits(
swapState.amount,
swapState.fromToken.decimals
);
if (!isNativeToken(swapState.fromToken)) {
const { hasEnoughAllowance, currentAllowance } =
await checkTokenAllowance(swapState.fromToken, amountNeeded);
setSwapState((prev) => ({
...prev,
isCheckingAllowance: false,
needsApproval: !hasEnoughAllowance,
allowanceAmount: currentAllowance,
}));
if (!hasEnoughAllowance) {
if (supportsPermit(swapState.fromToken)) {
try {
setSwapState((prev) => ({
...prev,
isApproving: true,
}));
setSwapState((prev) => ({
...prev,
isApproving: false,
needsApproval: false,
}));
} catch {
await approveToken(swapState.fromToken, amountNeeded);
await new Promise((resolve) => setTimeout(resolve, 2000));
setSwapState((prev) => ({
...prev,
isApproving: false,
needsApproval: false,
}));
}
} else {
setSwapState((prev) => ({
...prev,
isApproving: true,
}));
await approveToken(swapState.fromToken, amountNeeded);
await new Promise((resolve) => setTimeout(resolve, 2000));
setSwapState((prev) => ({
...prev,
isApproving: false,
needsApproval: false,
}));
}
}
} else {
setSwapState((prev) => ({
...prev,
isCheckingAllowance: false,
needsApproval: false,
}));
}
const txHash = await executeSwapQuote(swapState.quote);
setSwapState((prev) => ({
...prev,
isLoading: false,
txHash,
mayanSwapHash: null,
isExecuting: false,
}));
} catch (error) {
setSwapState((prev) => ({
...prev,
error:
error instanceof Error ? error.message : "Failed to execute swap",
isLoading: false,
isExecuting: false,
isCheckingAllowance: false,
isApproving: false,
}));
}
};
const handleClearError = () => {
setSwapState((prev) => ({ ...prev, error: null }));
};
const handleClearTxHash = () => {
setSwapState((prev) => ({ ...prev, txHash: null, mayanSwapHash: null }));
};
return (
<div className="max-w-4xl mx-auto p-6 mt-20">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Mayan Cross-Chain Swap
</h1>
<p className="text-gray-600">
Swap tokens across different blockchain networks using Mayan
</p>
</div>
<SwapForm
fromChain={swapState.fromChain}
toChain={swapState.toChain}
fromToken={swapState.fromToken}
toToken={swapState.toToken}
amount={swapState.amount}
chains={EVM_CHAINS}
fromTokens={fromTokens}
toTokens={toTokens}
isLoadingTokens={isLoadingTokens}
onFromChainChange={(chain) => {
setSwapState((prev) => ({
...prev,
fromChain: chain,
fromToken: null,
}));
if (chain) {
loadTokensForChain(chain.id, true);
}
}}
onToChainChange={(chain) => {
setSwapState((prev) => ({
...prev,
toChain: chain,
toToken: null,
}));
if (chain) {
loadTokensForChain(chain.id, false);
}
}}
onFromTokenChange={(token) =>
setSwapState((prev) => ({ ...prev, fromToken: token }))
}
onToTokenChange={(token) =>
setSwapState((prev) => ({ ...prev, toToken: token }))
}
onAmountChange={(amount) =>
setSwapState((prev) => ({ ...prev, amount }))
}
onRefreshTokens={loadTokensForChain}
/>
<ActionButtons
onGetQuote={handleGetQuote}
onExecuteSwap={handleExecuteSwap}
isLoading={swapState.isLoading}
isExecuting={swapState.isExecuting}
isCheckingAllowance={swapState.isCheckingAllowance}
isApproving={swapState.isApproving}
hasQuote={!!swapState.quote}
/>
{swapState.fromToken &&
!isNativeToken(swapState.fromToken) &&
swapState.quote && (
<div className="bg-blue-50 border border-blue-200 rounded-xl shadow-md p-4 mb-6">
<div className="flex items-center space-x-3">
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
<svg
className="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-blue-900">
Token Approval Status
</h3>
<p className="text-blue-700 mt-1">
{swapState.needsApproval
? `Approval needed for ${swapState.fromToken.symbol}. This allows Mayan to swap your tokens.`
: `${swapState.fromToken.symbol} is approved for swapping.`}
</p>
</div>
</div>
</div>
)}
<StatusMessages
error={swapState.error}
txHash={swapState.txHash}
mayanSwapHash={swapState.mayanSwapHash}
chainId={chainId}
onClearError={handleClearError}
onClearTxHash={handleClearTxHash}
/>
<RouteDisplay
quote={swapState.quote}
toTokenSymbol={swapState.toToken?.symbol}
/>
</div>
);
}