import { useState, useEffect, useMemo } from "react"; import { motion } from "framer-motion"; import { FiArrowDown } from "react-icons/fi"; import { useTheme } from "@/context"; import { useChains, useOdos } from "@/hooks"; import { Transaction } from "@/components/Web3/Transactions/transactions"; import { useMoonAccount } from "@/hooks"; import { formatUnits } from "ethers"; import { Button } from "@/components"; import { useQueryClient, useQuery } from "@tanstack/react-query"; import { safelyParseUnits } from "../../../utils"; import { useDebounceValue } from "usehooks-ts"; import { useErc20 } from "@/hooks/contracts/useErc20"; import { SlippageSettings } from "./SlippageSettings"; import { SwapDetails } from "./SwapDetails"; import { TokenInput } from "./TokenInput"; import type { InputBody } from "@moonup/moon-api"; type ButtonStatus = | "Estimate" | "Estimating..." | "Swapping..." | "Swap" | "Insufficient Balance"; const tokenPriceBaseUrl = "https://api.odos.xyz/pricing/token"; const fetchTokenPrice = async (chainId: number, tokenAddress: string) => { const response = await fetch( `${tokenPriceBaseUrl}/${chainId}/${tokenAddress}`, ); if (response.status === 200) { const data = await response.json(); return data.price; } else { throw new Error("Error fetching token price"); } }; const calculatePrice = (amount: string, price: number) => { return Number.parseFloat(amount) * price; }; const formatBalance = (balance: string, decimals: number) => { return formatUnits(balance, decimals); }; const fetchBalances = async (account: string, chainId: string) => { const response = await fetch( `https://api.odos.xyz/balances/${account}/${chainId}`, ); if (response.status === 200) { const data = await response.json(); console.log(data); return data.balances; } else { throw new Error("Error fetching balances"); } }; const fetchContractInfo = async (chainId: number) => { const response = await fetch( `https://api.odos.xyz/info/contract-info/v2/${chainId}`, ); if (response.status === 200) { const data = await response.json(); return data; } else { throw new Error("Error fetching contract info"); } }; export const OdosSwap = () => { const theme = useTheme(); const { getQuoteOdos, swapOdos, supportedTokensQuery } = useOdos(); const { account } = useMoonAccount(); const queryClient = useQueryClient(); const { selectedChain } = useChains(); const { approveTokenSpendIfNeeded } = useErc20(); const [fromToken, setFromToken] = useState(null); const [toToken, setToToken] = useState(null); const [fromAmount, setFromAmount] = useState(""); const [toAmount, setToAmount] = useState(""); const [slippage, setSlippage] = useState(0.5); const [estimateResult, setEstimateResult] = useState(null); const [buttonStatus, setButtonStatus] = useState("Estimate"); const [transactionData, setTransactionData] = useState( null, ); const [_, setError] = useState(null); const [isSwapComplete, setIsSwapComplete] = useState(false); const [showTransactionDetails, setShowTransactionDetails] = useState(false); const [contractInfo, setContractInfo] = useState(null); const [debouncedFromAmount] = useDebounceValue(fromAmount, 500); const { data: tokensFilteredForChain = [] } = supportedTokensQuery; const { data: balanceData = [] } = useQuery({ queryKey: ["OdosSwapBalances", account, selectedChain?.chain_id], queryFn: () => fetchBalances(account, selectedChain?.chain_id?.toString() || "1"), enabled: !!account && !!selectedChain, }); const tokensWithBalances = useMemo(() => { return tokensFilteredForChain.map((token) => { const balance = balanceData?.find( (b: any) => b.address === token.address, ); return { ...token, balance: balance ? formatBalance(balance.balance.toString(), token.decimals) : "0.00", }; }); }, [tokensFilteredForChain, balanceData]); const { data: fromTokenPrice, refetch: refetchFromTokenPrice } = useQuery({ queryKey: ["tokenPrice", fromToken], queryFn: () => fetchTokenPrice(selectedChain?.chain_id || 1, fromToken?.address), enabled: !!fromToken, }); const { data: toTokenPrice, refetch: refetchToTokenPrice } = useQuery({ queryKey: ["tokenPrice", toToken?.address], queryFn: () => fetchTokenPrice(selectedChain?.chain_id || 1, toToken?.address), enabled: !!toToken, }); useEffect(() => { if (fromToken && toToken) { refetchFromTokenPrice(); refetchToTokenPrice(); } }, [fromToken, toToken, refetchFromTokenPrice, refetchToTokenPrice]); const fromValue = calculatePrice(fromAmount, fromTokenPrice || 0); const toValue = calculatePrice(toAmount, toTokenPrice || 0); useEffect(() => { if (fromToken && toToken && debouncedFromAmount) { handleEstimate(); } }, [ fromToken, toToken, debouncedFromAmount, slippage, fromTokenPrice, toTokenPrice, ]); useEffect(() => { if (selectedChain && typeof selectedChain.chain_id === "number") { fetchContractInfo(selectedChain.chain_id) .then(setContractInfo) .catch((error) => console.error("Error fetching contract info:", error), ); } }, [selectedChain]); const handleEstimate = async () => { if (!fromToken || !toToken || !debouncedFromAmount) return; setButtonStatus("Estimating..."); const bigAmountWei = safelyParseUnits( debouncedFromAmount, fromToken.decimals, ); try { const swapParams = { inputTokens: [ { amount: bigAmountWei.toString(), tokenAddress: fromToken.address }, ], outputTokens: [{ proportion: 1, tokenAddress: toToken.address }], slippageLimitPercent: Math.round(slippage * 100), chain_id: selectedChain?.chain_id?.toString() || "1", }; const estimateResult = await getQuoteOdos({ accountName: account || "", data: swapParams, }); setEstimateResult(estimateResult); setToAmount( estimateResult?.data ? formatUnits(estimateResult.data.outAmounts[0], toToken.decimals) : "0", ); setButtonStatus("Swap"); } catch (error) { console.error("Estimate failed:", error); setButtonStatus("Estimate"); } }; useEffect(() => { const interval = setInterval(() => { handleEstimate(); }, 60000); return () => clearInterval(interval); }, [fromToken, toToken, debouncedFromAmount, slippage]); const handleSwap = async () => { if ( !fromToken || !toToken || !fromAmount || !toAmount || !account || !contractInfo ) return; setButtonStatus("Swapping..."); const bigAmountWei = safelyParseUnits(fromAmount, fromToken.decimals); try { // Check if fromToken is not the native token if (fromToken.address !== "0x0000000000000000000000000000000000000000") { await approveTokenSpendIfNeeded( account, contractInfo.routerAddress, fromToken.address, Number.parseFloat(fromAmount), selectedChain?.chain_id?.toString() || "1", ); } const swapParams = { inputTokens: [ { amount: bigAmountWei.toString(), tokenAddress: fromToken.address }, ], outputTokens: [{ proportion: 1, tokenAddress: toToken.address }], slippageLimitPercent: Math.round(slippage * 100), chain_id: selectedChain?.chain_id?.toString() || "1", broadcast: false, dryrun: true, }; const result = (await swapOdos({ accountName: account, data: swapParams, })) as any; queryClient.invalidateQueries({ queryKey: ["erc20TokenBalances"] }); console.log(result); setTransactionData({ to: result.transaction.to, data: result.transaction.data, value: result.transaction.value, nonce: result.transaction.nonce, gasLimit: result.transaction.gasLimit, maxFeePerGas: result.transaction.maxFeePerGas, maxPriorityFeePerGas: result.transaction.maxPriorityFeePerGas, chain_id: selectedChain?.chain_id?.toString() || "1", }); setShowTransactionDetails(true); setButtonStatus("Estimate"); } catch (error) { console.error("Swap failed:", error); setError((error as Error)?.message || "Swap failed"); setButtonStatus("Swap"); } }; const swapTokens = () => { setFromToken(toToken); setToToken(fromToken); setFromAmount(toAmount); setToAmount(fromAmount); }; const handleSwapComplete = () => { setIsSwapComplete(true); setFromAmount(""); setToAmount(""); setButtonStatus("Estimate"); setShowTransactionDetails(false); }; const handleError = (error: any) => { console.log("transaction error called", error); setError(error); setButtonStatus("Estimate"); setTransactionData(null); setShowTransactionDetails(false); setEstimateResult(null); setFromAmount(""); setToAmount(""); // Reset other state variables if needed }; if (isSwapComplete) { return (

Swap Complete!

Your transaction has been successfully processed.

); } return (
{!showTransactionDetails && ( <>

Swap

{estimateResult && ( )} )} {transactionData && ( )}
); }; export default OdosSwap;