import React, { useState, useEffect, useMemo } from "react"; import { motion } from "framer-motion"; import { FiArrowDown } from "react-icons/fi"; import { useTheme } from "@/context"; import { useLifi, useMoonAccount, useMoonDataSDK } from "@/hooks"; import { Transaction } from "@/components/Web3/Transactions/transactions"; import { formatUnits, parseUnits } from "ethers"; import { Button } from "@/components"; import { useQueryClient } from "@tanstack/react-query"; import { useDebounceValue } from "usehooks-ts"; import { useErc20 } from "@/hooks"; import { TokenInput } from "./TokenInput"; import { ChainSelector } from "./ChainSelector"; import { SlippageSettings } from "./SlippageSettings"; import { SwapDetails } from "./SwapDetails"; import type { InputBody, Quote } from "@moonup/moon-api"; type ButtonStatus = | "Get Quote" | "Getting Quote..." | "Bridging..." | "Bridge" | "Insufficient Balance" | "Enter an amount" | "Select tokens and chains"; export const LifiBridge: React.FC = () => { const theme = useTheme(); const { getQuoteLifi, postQuoteLifi, supportedTokensQuery, supportedChainsQuery, } = useLifi(); const { account } = useMoonAccount(); const queryClient = useQueryClient(); const { approveTokenSpendIfNeeded } = useErc20(); const { walletBalanceQuery, walletBalanceChainIdQuery } = useMoonDataSDK(); const [fromToken, setFromToken] = useState(null); const [toToken, setToToken] = useState(null); const [fromChain, setFromChain] = useState(null); const [toChain, setToChain] = useState(null); const [fromAmount, setFromAmount] = useState(""); const [toAmount, setToAmount] = useState(""); const [slippage, setSlippage] = useState(0.5); const [quoteResult, setQuoteResult] = useState(null); const [buttonStatus, setButtonStatus] = useState("Get Quote"); const [transactionData, setTransactionData] = useState( null, ); const [error, setError] = useState(null); const [isSwapComplete, setIsSwapComplete] = useState(false); const [showTransactionDetails, setShowTransactionDetails] = useState(false); const [debouncedFromAmount] = useDebounceValue(fromAmount, 500); const { data: walletBalanceData = { balances: {} } } = walletBalanceQuery; const { data: walletBalanceChainIdData = { balances: {} } } = walletBalanceChainIdQuery(toChain?.id ?? 1); const { data: tokensData = { tokens: {} } } = supportedTokensQuery; const { data: chainsData = { chains: [] } } = supportedChainsQuery; const tokensFilteredForChain = useMemo(() => { if (!fromChain) return []; // add the balance to the token object const tokens = tokensData.tokens[fromChain.id.toString()] || []; return tokens.map((token: any) => { const balances: { [key: string]: string } = Array.isArray( walletBalanceData, ) ? {} : walletBalanceData?.balances || {}; token.balance = balances[token.address] || "0"; return token; }); }, [tokensData, fromChain, walletBalanceData]); const tokensFilteredToChain = useMemo(() => { if (!toChain) return []; const tokens = tokensData.tokens[toChain.id.toString()] || []; return tokens.map((token: any) => { const balances: { [key: string]: string } = Array.isArray( walletBalanceChainIdData, ) ? {} : walletBalanceChainIdData?.balances || {}; token.balance = balances[token.address] || "0"; return token; }); }, [tokensData, toChain]); const fromValue = useMemo(() => { return quoteResult ? Number.parseFloat(quoteResult.estimate.fromAmountUSD) : 0; }, [quoteResult]); const toValue = useMemo(() => { return quoteResult ? Number.parseFloat(quoteResult.estimate.toAmountUSD) : 0; }, [quoteResult]); const isSwapDisabled = useMemo(() => { if (!fromToken || !toToken || !fromChain || !toChain) { return "Select tokens and chains"; } if (!fromAmount || Number.parseFloat(fromAmount) === 0) { return "Enter an amount"; } if ( Number.parseFloat(fromAmount) > Number.parseFloat(fromToken.balance || "0") ) { return "Insufficient Balance"; } if (!quoteResult) { return "Get Quote"; } return false; }, [fromToken, toToken, fromChain, toChain, fromAmount, quoteResult]); useEffect(() => { if (fromToken && toToken && fromChain && toChain && debouncedFromAmount) { handleGetQuote(); } else { setQuoteResult(null); setToAmount(""); } }, [fromToken, toToken, fromChain, toChain, debouncedFromAmount, slippage]); const handleGetQuote = async () => { if ( !fromToken || !toToken || !fromChain || !toChain || !debouncedFromAmount ) return; setButtonStatus("Getting Quote..."); setError(null); const fromAmountBigInt = parseUnits( debouncedFromAmount, fromToken.decimals, ); try { const quoteParams = { fromChain: fromChain.key, toChain: toChain.key, fromToken: fromToken.address, toToken: toToken.address, fromAmount: fromAmountBigInt.toString(), fromAddress: account || "", slippage: slippage, }; const quote = await getQuoteLifi(quoteParams); setQuoteResult(quote); setToAmount(formatUnits(quote.estimate.toAmount, toToken.decimals)); setButtonStatus("Bridge"); } catch (error) { console.error("Quote failed:", error); setError((error as Error)?.message || "Failed to get quote"); setButtonStatus("Get Quote"); } }; const handleBridge = async () => { if (!quoteResult || !account) return; setButtonStatus("Bridging..."); try { if (fromToken.address !== "0x0000000000000000000000000000000000000000") { await approveTokenSpendIfNeeded( account, quoteResult.transactionRequest.to || "", fromToken.address, Number.parseFloat(fromAmount), fromChain.chainId.toString(), ); } const bridgeParams = { accountName: account, fromAmount: quoteResult.estimate.fromAmount, fromChain: fromChain.key, fromToken: fromToken.address, toAmount: quoteResult.estimate.toAmount, toChain: toChain.key, toToken: toToken.address, fromAddress: account, toAddress: account, action: quoteResult.action, estimate: quoteResult.estimate, id: quoteResult.id, includedSteps: quoteResult.includedSteps, tool: quoteResult.tool, transactionRequest: quoteResult.transactionRequest, type: quoteResult.type, }; const result = await postQuoteLifi(bridgeParams); queryClient.invalidateQueries({ queryKey: ["erc20TokenBalances"] }); setTransactionData({ to: result.quote.transactionRequest.to, data: result.quote.transactionRequest.data, value: result.quote.transactionRequest.value, chain_id: fromChain.chainId.toString(), maxFeePerGas: result.quote.transactionRequest.maxFeePerGas, maxPriorityFeePerGas: result.quote.transactionRequest.maxPriorityFeePerGas, nonce: result.quote.transactionRequest.nonce, }); setShowTransactionDetails(true); setButtonStatus("Get Quote"); } catch (error) { console.error("Bridge failed:", error); setError((error as Error)?.message || "Bridge failed"); setButtonStatus("Bridge"); } }; const handleSwapComplete = () => { setIsSwapComplete(true); setFromAmount(""); setToAmount(""); setButtonStatus("Get Quote"); setShowTransactionDetails(false); }; const handleError = (error: any) => { console.log("transaction error called", error); setError(error); setButtonStatus("Get Quote"); setTransactionData(null); setShowTransactionDetails(false); setQuoteResult(null); setFromAmount(""); setToAmount(""); }; if (isSwapComplete) { return (

Bridge Complete!

Your transaction has been successfully processed.

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

Lifi Bridge

{error &&

{error}

} {quoteResult && ( )} )} {transactionData && ( )}
); }; export default LifiBridge;