import React, { useState, useEffect, useMemo, useRef } from "react"; import { useAccount } from "wagmi"; import { useGarden } from "@gardenfi/react-hooks"; import InputField from "./InputField"; import { swapStore } from "../../store/swapStore"; import { ExchangeIcon, Button, Typography } from "@gardenfi/garden-book"; import BigNumber from "bignumber.js"; import { MatchedOrder } from "@gardenfi/orderbook"; import { toast } from "react-toastify"; import { IOType } from "../../constants/constants"; import { assetInfoStore } from "../../store/assetInfoStore"; import { constructOrderPair } from "@gardenfi/core"; import { formatAmount, trimAddress } from "../../utils/utils"; import { ErrorFormat, Errors } from "../../constants/errors"; const Swap: React.FC = () => { const [orderDetails, setOrderDetails] = useState(); const [openDropdown, setOpenDropdown] = useState(null); const debounceTimeoutRef = useRef(null); const { inputAmount, outputAmount, inputAsset, outputAsset, error, btcAddress, loading, fetching, showBtcAddress, setBtcAddress, setShowBtcAddress, setError, setAmount, swapAssets, setStrategy, setLoading, setFetching, } = swapStore(); const { strategies, fetchAndSetStrategies } = assetInfoStore(); const { address: evmAddress } = useAccount(); const { getQuote, swapAndInitiate } = useGarden(); useEffect(() => { fetchAndSetStrategies(); }, [fetchAndSetStrategies]); const { minAmount, maxAmount } = useMemo(() => { const defaultLimits = { minAmount: 0, maxAmount: 0, }; if (!inputAsset || !outputAsset || !strategies.val) return defaultLimits; const limits = strategies.val[ constructOrderPair( inputAsset.chain, inputAsset.atomicSwapAddress, outputAsset.chain, outputAsset.atomicSwapAddress ) ]; if (!limits) return defaultLimits; else{ return { minAmount: formatAmount( limits.minAmount, inputAsset.decimals, ), maxAmount: formatAmount( limits.maxAmount, inputAsset.decimals, ), } ; } }, [inputAsset, outputAsset, strategies.val]); const validateInput = (value: number): ErrorFormat | undefined => { if (!value || value <= 0) { setAmount(IOType.output, "0"); return Errors.none; } if (inputAsset && minAmount && value < minAmount) { return Errors.minError(minAmount.toString(), inputAsset.symbol); } if (inputAsset && maxAmount && value > maxAmount) { return Errors.maxError(maxAmount.toString(), inputAsset.symbol); } return Errors.none; }; const handleInputChange = async (value: string) => { if (!getQuote) return; if (/^[0-9]*\.?[0-9]*$/.test(value)) { setAmount(IOType.input, value); const numericValue = parseFloat(value); const validationError = validateInput(numericValue); setError({ inputError: validationError }); if (!validationError && numericValue) { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } debounceTimeoutRef.current = setTimeout(() => { fetchQuote(value); }, 500); } else if (value === "") { setAmount(IOType.output, "0"); } } }; const fetchQuote = async (amount: string) => { if (!getQuote || !inputAsset || !outputAsset) return; setFetching(true); try { const amountInDecimals = new BigNumber(amount).multipliedBy( 10 ** inputAsset.decimals ); const quote = await getQuote({ fromAsset: inputAsset, toAsset: outputAsset, amount: amountInDecimals.toNumber(), isExactOut: false, }); if (quote?.val?.quotes) { const [strategy, quoteAmount] = Object.entries(quote.val.quotes)[0] as [ string, string ]; const quoteAmountInDecimals = new BigNumber(quoteAmount).div( 10 ** outputAsset.decimals ); setAmount(IOType.input, amount); setAmount(IOType.output, quoteAmountInDecimals.toFixed(outputAsset.decimals, BigNumber.ROUND_DOWN)); setStrategy(strategy); } else { setAmount(IOType.output, "0"); } } catch (error) { console.error("Error fetching quote:", error); setAmount(IOType.output, "0"); } finally { setFetching(false); } }; const performSwap = async (strategyId: string, receiveAmount: string) => { if (!swapAndInitiate) throw new Error("Swap service unavailable."); const sendAmount = ( new BigNumber(inputAmount).times(10 ** inputAsset!.decimals) ).toString(); const res = await swapAndInitiate({ fromAsset: inputAsset!, toAsset: outputAsset!, sendAmount, receiveAmount, additionalData: { btcAddress, strategyId, }, }); if (res?.error) throw new Error(res.error); return res; }; const handleSwap = async () => { if (!getQuote || !swapAndInitiate) return; const isBtcSelected = inputAsset!.symbol === "BTC" || outputAsset!.symbol === "BTC"; if (isBtcSelected && !btcAddress) { toast.error("Please provide a Bitcoin address."); return; } if (!evmAddress || new BigNumber(inputAmount).isLessThanOrEqualTo(0)) { toast.error("Please fill in all fields correctly."); return; } setLoading(true); try { const { strategyId, receiveAmount } = await fetchSwapQuote(); const swapResult = await performSwap(strategyId, receiveAmount); toast.success("Swap successful!"); setOrderDetails(swapResult.val); } catch (error) { toast.error(error instanceof Error ? error.message : "An error occurred."); } finally { setLoading(false); } }; const fetchSwapQuote = async () => { if (!getQuote) throw new Error("Quote service unavailable."); const sendAmount = new BigNumber(inputAmount).times(10 ** inputAsset!.decimals).toString(); const quote = await getQuote({ fromAsset: inputAsset!, toAsset: outputAsset!, amount: Number(sendAmount), }); if (!quote?.val) throw new Error(quote?.error || "Error fetching quote."); const [strategyId, receiveAmount] = Object.entries(quote.val.quotes)[0] as [ string, string ]; return { strategyId, receiveAmount }; }; useEffect(() => { if (inputAsset?.symbol === "BTC" || outputAsset?.symbol === "BTC") { setShowBtcAddress(true); } else { setShowBtcAddress(false); } }, [inputAsset, outputAsset]); useEffect(() => { if (inputAsset && outputAsset && inputAmount && parseFloat(inputAmount) > 0) { handleInputChange(inputAmount); } else { setAmount(IOType.output, "0"); } }, [inputAsset, outputAsset]); useEffect(() => { return () => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } }; }, []); const buttonVariant = useMemo(() => { const numericValue = parseFloat(inputAmount); if (loading || fetching || error.inputError || !inputAmount || numericValue <= 0) { return "disabled"; } return "primary"; }, [loading, fetching, error.inputError, inputAmount]); const buttonText = useMemo(() => { if (loading) return "Processing..."; if (fetching) return "Fetching..."; if (error.inputError && typeof error.inputError === "string") { return error.inputError; } return "Swap"; }, [loading, fetching, error.inputError]); const isButtonDisabled = useMemo(() => { const numericValue = parseFloat(inputAmount); return loading || fetching || !!error.inputError || !inputAmount || numericValue <= 0; }, [loading, fetching, error.inputError, inputAmount]); return (
Swap Assets
{showBtcAddress && ( )} {orderDetails && (
Order ID: window.open( `https://testnet-explorer.garden.finance/order/${orderDetails?.create_order.create_id}`, "_blank" ) } className="text-blue-500 underline hover:text-blue-400 cursor-pointer" > {trimAddress(orderDetails?.create_order.create_id)}
)}
); }; export default Swap;