From 5ceb52ebbc0e0423afc16f7c250cf02a5242db8a Mon Sep 17 00:00:00 2001 From: denis-orbs Date: Wed, 30 Oct 2024 17:17:51 +0200 Subject: [PATCH] a --- src/components/swap-details.tsx | 64 - src/components/tokens/token-card.tsx | 84 +- src/components/tokens/token-select.tsx | 22 +- src/lib/useGetRequiresApproval.ts | 2 +- src/lib/useHandleInputError.ts | 13 +- src/lib/useParaswap.ts | 4 +- src/lib/usePriceUsd.ts | 6 +- src/lib/useTokensWithBalances.ts | 33 +- src/lib/utils.ts | 37 +- src/trade/liquidity-hub/context.tsx | 107 ++ src/trade/liquidity-hub/hooks.ts | 198 +++ .../liquidity-hub-confirmation-dialog.tsx | 709 ++++++++--- .../liquidity-hub/liquidity-hub-swap.tsx | 1094 ++++------------- .../twap/components/limit-price-input.tsx | 4 +- src/trade/twap/components/src-chunk-size.tsx | 7 +- src/trade/twap/hooks.ts | 15 +- src/trade/twap/twap-confirmation-dialog.tsx | 5 +- src/trade/twap/twap.tsx | 6 - 18 files changed, 1215 insertions(+), 1195 deletions(-) delete mode 100644 src/components/swap-details.tsx create mode 100644 src/trade/liquidity-hub/context.tsx create mode 100644 src/trade/liquidity-hub/hooks.ts diff --git a/src/components/swap-details.tsx b/src/components/swap-details.tsx deleted file mode 100644 index f5245b1..0000000 --- a/src/components/swap-details.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { DataDetails } from '@/components/ui/data-details' -import { Separator } from '@/components/ui/separator' -import { format, fromBigNumber, getLiquidityProviderName } from '@/lib' -import { LiquidityProvider, Token } from '@/types' -import { OptimalRate } from '@paraswap/sdk' -import { useMemo } from 'react' - -export type SwapDetailsProps = { - optimalRate?: OptimalRate - inToken: Token | null - outToken: Token | null - account?: string - minAmountOut?: string - isLiquidityHubTrade: boolean -} - -export function SwapDetails({ - inToken, - outToken, - account, - minAmountOut, - optimalRate, - isLiquidityHubTrade, -}: SwapDetailsProps) { - const inPriceUsd = useMemo(() => { - if (!optimalRate) return 0 - const amount = fromBigNumber(optimalRate.srcAmount, inToken?.decimals) - return Number(optimalRate.srcUSD) / Number(amount) - }, [optimalRate, inToken]) - - const outPriceUsd = useMemo(() => { - if (!optimalRate) return 0 - const amount = fromBigNumber(optimalRate.destAmount, outToken?.decimals) - return Number(optimalRate.destUSD) / Number(amount) - }, [optimalRate, outToken]) - - if (!inToken || !outToken || !account || !optimalRate) return null - - const rate = inPriceUsd / outPriceUsd - - let data: Record = { - Rate: `1 ${inToken.symbol} ≈ ${format.crypto(rate)} ${outToken.symbol}`, - } - - const minOutAmount = fromBigNumber(minAmountOut, outToken.decimals) - const outAmount = fromBigNumber(optimalRate.destAmount, outToken.decimals) - data = { - ...data, - 'Est. Received': `${format.crypto(Number(outAmount))} ${outToken.symbol}`, - 'Min. Received': `${format.crypto(minOutAmount)} ${outToken.symbol}`, - 'Routing source': getLiquidityProviderName(isLiquidityHubTrade), - } - - return ( -
- - -
-
Recepient
-
{format.address(account)}
-
-
- ) -} diff --git a/src/components/tokens/token-card.tsx b/src/components/tokens/token-card.tsx index c437d87..c2d4d5b 100644 --- a/src/components/tokens/token-card.tsx +++ b/src/components/tokens/token-card.tsx @@ -1,72 +1,75 @@ -import { WalletIcon } from 'lucide-react' -import { Card } from '../ui/card' -import { TokenSelect } from './token-select' -import { Token, TokensWithBalances } from '@/types' -import { NumericFormat } from 'react-number-format' +import { WalletIcon } from "lucide-react"; +import { Card } from "../ui/card"; +import { TokenSelect } from "./token-select"; +import { Token } from "@/types"; +import { NumericFormat } from "react-number-format"; import { format, cn, - fromBigNumber, ErrorCodes, -} from '@/lib' -import { Skeleton } from '../ui/skeleton' -import { Button } from '../ui/button' -import { useToExactAmount } from '@/trade/hooks' -import BN from 'bignumber.js' + useTokensWithBalances, + useTokenBalance, + toExactAmount, +} from "@/lib"; +import { Skeleton } from "../ui/skeleton"; +import { Button } from "../ui/button"; +import { useToExactAmount } from "@/trade/hooks"; +import BN from "bignumber.js"; function getTextSize(amountLength: number) { if (amountLength > 16) { - return 'text-xl' + return "text-xl"; } if (amountLength > 12 && amountLength <= 16) { - return 'text-2xl' + return "text-2xl"; } - return 'text-4xl' + return "text-4xl"; } export type TokenCardProps = { - label: string - amount: string - amountUsd?: string - balance: any - selectedToken: Token - tokens: TokensWithBalances - onSelectToken: (token: Token) => void - isAmountEditable?: boolean - onValueChange?: (value: string) => void - amountLoading?: boolean - inputError?: string | null -} + label: string; + amount: string; + amountUsd?: string; + selectedToken: Token | null; + onSelectToken: (token: Token) => void; + isAmountEditable?: boolean; + onValueChange?: (value: string) => void; + amountLoading?: boolean; + inputError?: string | null; +}; export function TokenCard({ label, amount, amountUsd, - balance, selectedToken, - tokens, onSelectToken, onValueChange, isAmountEditable = true, amountLoading, inputError, }: TokenCardProps) { - - const balanceError = inputError === ErrorCodes.InsufficientBalance + const balance = useTokenBalance(selectedToken?.address); + const balanceError = inputError === ErrorCodes.InsufficientBalance; const balanceDisplay = selectedToken - ? format.crypto(fromBigNumber(balance, selectedToken.decimals)) - : '0' + ? format.crypto(Number(toExactAmount(balance, selectedToken.decimals))) + : "0"; - const maxBalance = useToExactAmount(balance, selectedToken?.decimals) - const halfBalance = useToExactAmount(BN(balance || 0).dividedBy(2).toString(), selectedToken?.decimals) + const maxBalance = useToExactAmount(balance, selectedToken?.decimals); + const halfBalance = useToExactAmount( + BN(balance || 0) + .dividedBy(2) + .toString(), + selectedToken?.decimals + ); return (
@@ -97,7 +100,7 @@ export function TokenCard({ {amountLoading ? ( ) : ( -
+
@@ -126,7 +128,7 @@ export function TokenCard({
) : (
- {format.dollar(Number(amountUsd || '0'))} + {format.dollar(Number(amountUsd || "0"))}
)}
@@ -135,5 +137,5 @@ export function TokenCard({
- ) + ); } diff --git a/src/components/tokens/token-select.tsx b/src/components/tokens/token-select.tsx index cbeeca4..fd919e2 100644 --- a/src/components/tokens/token-select.tsx +++ b/src/components/tokens/token-select.tsx @@ -10,26 +10,27 @@ import { } from "../ui/dialog"; import { Input } from "../ui/input"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; -import { Token, TokensWithBalances } from "@/types"; +import { Token } from "@/types"; import { Card } from "../ui/card"; import { useMemo, useState } from "react"; -import { fromBigNumber } from "@/lib"; +import { format, toExactAmount, useTokensWithBalances } from "@/lib"; +import BN from "bignumber.js"; type TokenSelectProps = { selectedToken: Token | undefined; - tokens: TokensWithBalances; onSelectToken: (token: Token) => void; }; export function TokenSelect({ selectedToken, - tokens, onSelectToken, }: TokenSelectProps) { const [open, setOpen] = useState(false); + const tokens = useTokensWithBalances().tokensWithBalances const [filterInput, setFilterInput] = useState(""); const SortedTokens = useMemo(() => { + if(!tokens) return null; return Object.values(tokens) .filter((t) => { return ( @@ -37,11 +38,12 @@ export function TokenSelect({ t.token.address.toLowerCase().includes(filterInput.toLowerCase()) ); }) - .sort( - (a, b) => - fromBigNumber(b.balance, b.token.decimals) - - fromBigNumber(a.balance, a.token.decimals) - ) + .sort((a, b) => { + + const balanceA = BN(toExactAmount(a.balance.toString(), a.token.decimals)); + const balanceB = BN(toExactAmount(b.balance.toString(), b.token.decimals)); + return balanceB.minus(balanceA).toNumber(); + }) .map((t) => ( {t.token.name} -
{fromBigNumber(t.balance, t.token.decimals).toFixed(5)}
+
{format.crypto(Number(toExactAmount(t.balance.toString(), t.token.decimals) || '0'))}
)); }, [filterInput, onSelectToken, tokens]); diff --git a/src/lib/useGetRequiresApproval.ts b/src/lib/useGetRequiresApproval.ts index 99c00a7..8d0988b 100644 --- a/src/lib/useGetRequiresApproval.ts +++ b/src/lib/useGetRequiresApproval.ts @@ -3,7 +3,7 @@ import { Address, erc20Abi } from 'viem' /* Determines whether user needs tp approve allowance for quoted token */ export function useGetRequiresApproval( - contractAddress: Address, + contractAddress?: Address, inTokenAddress = '', inAmount = '' ) { diff --git a/src/lib/useHandleInputError.ts b/src/lib/useHandleInputError.ts index 5e4a806..2ee712a 100644 --- a/src/lib/useHandleInputError.ts +++ b/src/lib/useHandleInputError.ts @@ -1,10 +1,10 @@ -import { ErrorCodes, fromBigNumber } from "@/lib/utils"; +import { ErrorCodes, toExactAmount } from "@/lib/utils"; import { Token } from "@/types"; import { useMemo } from "react"; import { useTokenBalance, - useTokensWithBalances, } from "./useTokensWithBalances"; +import BN from "bignumber.js"; /* Handles amount input errors */ @@ -15,18 +15,15 @@ export function useInputError({ inToken: Token | null; inputAmount: string; }) { - const tokensWithBalances = useTokensWithBalances(); const tokenBalance = useTokenBalance(inToken?.address); return useMemo(() => { - if (!inToken || !tokensWithBalances) return; if (!inputAmount) { return ErrorCodes.EnterAmount; } - const value = Number(inputAmount); - const balance = fromBigNumber(tokenBalance, inToken.decimals); + const balance = toExactAmount(tokenBalance, inToken?.decimals); - if (value > balance) { + if (BN(inputAmount).gt(balance)) { return ErrorCodes.InsufficientBalance; } - }, [inputAmount, inToken, tokenBalance, tokensWithBalances]); + }, [inputAmount, inToken, tokenBalance]); } diff --git a/src/lib/useParaswap.ts b/src/lib/useParaswap.ts index 8043cc5..47eb488 100644 --- a/src/lib/useParaswap.ts +++ b/src/lib/useParaswap.ts @@ -35,10 +35,12 @@ export const useParaswapQuote = ({ inToken, outToken, inAmount, + refetchInterval = 30_000, }: { inToken?: string outToken?: string inAmount?: string + refetchInterval?: number }) => { const paraswap = useParaswap() const { chainId } = useAccount() @@ -72,7 +74,7 @@ export const useParaswapQuote = ({ return dexQuote }, enabled: !!inToken && !!outToken && Number(inAmount) > 0, - refetchInterval: 30_000, + refetchInterval, }) } diff --git a/src/lib/usePriceUsd.ts b/src/lib/usePriceUsd.ts index f807841..e81a13d 100644 --- a/src/lib/usePriceUsd.ts +++ b/src/lib/usePriceUsd.ts @@ -1,11 +1,13 @@ import { networks, isNativeAddress } from '@/lib' import { useQuery } from '@tanstack/react-query' +import { useAccount } from 'wagmi' -export const usePriceUsd = (chainId: number, address?: string) => { +export const usePriceUsd = (address?: string) => { + const {chainId} = useAccount() return useQuery({ queryKey: ['usePriceUSD', chainId, address], queryFn: async () => { - if (!address) { + if (!address || !chainId) { return 0 } diff --git a/src/lib/useTokensWithBalances.ts b/src/lib/useTokensWithBalances.ts index 9a93cf4..c1d2bdc 100644 --- a/src/lib/useTokensWithBalances.ts +++ b/src/lib/useTokensWithBalances.ts @@ -1,31 +1,32 @@ -import { useAccount } from 'wagmi' -import { useTokensList } from './useTokenList' -import { useBalances } from './useBalances' -import { networks } from '@/lib/networks' +import { useAccount } from "wagmi"; +import { useTokensList } from "./useTokenList"; +import { useBalances } from "./useBalances"; +import { TokensWithBalances } from "@/types"; export function useTokensWithBalances() { - const account = useAccount() - const { data: tokens, isLoading: tokensLoading } = useTokensList() + const { address, chainId } = useAccount(); + const { data: tokens, isLoading: tokensLoading } = useTokensList(); const { query: { data: balances, isLoading: balancesLoading, refetch }, queryKey, } = useBalances({ - chainId: networks.poly.id, + chainId, tokens: tokens || [], - account: account.address, - enabled: Boolean(tokens && account.address), - }) + account: address, + enabled: Boolean(tokens && address && chainId), + }); return { isLoading: tokensLoading || balancesLoading, - tokensWithBalances: balances, + tokensWithBalances: balances as TokensWithBalances, queryKey, refetch, - } + }; } - export const useTokenBalance = (tokenAddress?: string) => { - const { tokensWithBalances } = useTokensWithBalances() - return !tokenAddress ? '' : tokensWithBalances?.[tokenAddress]?.balance.toString() -} \ No newline at end of file + const { tokensWithBalances } = useTokensWithBalances(); + return !tokenAddress + ? "" + : tokensWithBalances?.[tokenAddress]?.balance.toString(); +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6c0b42d..58f6527 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -10,12 +10,6 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export const toBigInt = (amount: string | number, decimals?: number) => { - if (!amount) return BigInt(0); - const num = Number(amount); - return BigInt((num * 10 ** (decimals || 0)).toFixed(0)); -}; - export const toExactAmount = ( amount?: string, decimals?: number, @@ -34,32 +28,6 @@ export const toRawAmount = (amount?: string, decimals?: number) => { return BN(amount).times(BN(10).pow(decimals)).decimalPlaces(0).toFixed(); }; -export const fromBigNumberToStr = ( - amount: bigint | string, - decimals?: number -) => { - const numStr = typeof amount === "bigint" ? amount.toString() : amount; - const precision = decimals || 0; - - if (precision > 0) { - const integerPart = numStr.slice(0, -precision) || "0"; - const fractionalPart = numStr.slice(-precision).padStart(precision, "0"); - - return `${integerPart}.${fractionalPart}`; - } else { - return numStr; - } -}; - -export const fromBigNumber = ( - amount: bigint | string | undefined | null, - decimals?: number -) => { - if (amount === null || typeof amount === "undefined") return 0; - - return Number(fromBigNumberToStr(amount, decimals)); -}; - export const nativeTokenAddresses = [ zeroAddress, "0x0000000000000000000000000000000000001010", @@ -104,6 +72,8 @@ export const format = { }; export const getMinAmountOut = (slippage: number, _destAmount: string) => { + console.log(_destAmount); + const slippageFactor = BigInt(1000 - Math.floor(slippage * 10)); // 0.5% becomes 995 // Convert priceRoute.destAmount to BigInt @@ -118,7 +88,8 @@ export const enum ErrorCodes { EnterAmount = "Enter amount", } -export function getQuoteErrorMessage(errorCode: string) { +export function getQuoteErrorMessage(errorCode?: string) { + if (!errorCode) return ""; switch (errorCode) { case "ldv": return "Minimum trade amount is $30"; diff --git a/src/trade/liquidity-hub/context.tsx b/src/trade/liquidity-hub/context.tsx new file mode 100644 index 0000000..948b1ab --- /dev/null +++ b/src/trade/liquidity-hub/context.tsx @@ -0,0 +1,107 @@ +import { useDefaultTokens, useTokensWithBalances } from "@/lib"; +import { Token } from "@/types"; +import { constructSDK, LiquidityHubSDK, Quote } from "@orbs-network/liquidity-hub-sdk"; +import { useContext, ReactNode, useReducer, useCallback, useMemo, createContext } from "react"; +import { useAccount } from "wagmi"; +import { useToRawAmount } from "../hooks"; + +const initialState: State = { + inToken: null, + outToken: null, + inputAmount: "", + acceptedQuote: undefined, + liquidityHubDisabled: false, + slippage: 0.5, + forceLiquidityHub: false, + showConfirmation: false, + }; + + interface State { + inToken: Token | null; + outToken: Token | null; + inputAmount: string; + acceptedQuote: Quote | undefined; + liquidityHubDisabled: boolean; + slippage: number; + forceLiquidityHub: boolean; + showConfirmation: boolean; + signature?: string; + isLiquidityHubTrade?: boolean; + } + + type Action = { type: "UPDATE"; payload: Partial } | { type: "RESET" }; + + const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "UPDATE": + return { ...state, ...action.payload }; + case "RESET": + return initialState; + default: + return state; + } + }; + + interface ContextType { + state: State; + updateState: (payload: Partial) => void; + resetState: () => void; + sdk: LiquidityHubSDK; + parsedInputAmount?: string; + } + + const Context = createContext({} as ContextType); + export const useLiquidityHubSwapContext = () => { + return useContext(Context); + }; + + + export const LiquidityHubSwapProvider = ({ children }: { children: ReactNode }) => { + const [state, dispatch] = useReducer(reducer, initialState); + const { chainId } = useAccount(); + const { tokensWithBalances, refetch: refetchBalances } = + useTokensWithBalances(); + const parsedInputAmount = useToRawAmount( + state.inputAmount, + state.inToken?.decimals + ); + + const updateState = useCallback( + (payload: Partial) => { + dispatch({ type: "UPDATE", payload }); + }, + [dispatch] + ); + + const resetState = useCallback(() => { + dispatch({ type: "RESET" }); + refetchBalances(); + }, [dispatch, refetchBalances]); + + const sdk = useMemo( + () => constructSDK({ partner: "widget", chainId }), + [chainId] + ); + + useDefaultTokens({ + inToken: state.inToken, + outToken: state.outToken, + tokensWithBalances, + setInToken: (token) => updateState({ inToken: token }), + setOutToken: (token) => updateState({ outToken: token }), + }); + + return ( + + {children} + + ); + }; \ No newline at end of file diff --git a/src/trade/liquidity-hub/hooks.ts b/src/trade/liquidity-hub/hooks.ts new file mode 100644 index 0000000..54a8e34 --- /dev/null +++ b/src/trade/liquidity-hub/hooks.ts @@ -0,0 +1,198 @@ +import { + resolveNativeTokenAddress, + useWrapOrUnwrapOnly, + useParaswapQuote, + useInputError, + getMinAmountOut, + useGetRequiresApproval, + network, + networks, + isNativeAddress, +} from "@/lib"; +import { permit2Address } from "@orbs-network/liquidity-hub-sdk"; +import { useQueryClient, useQuery } from "@tanstack/react-query"; +import { useMemo, useCallback } from "react"; +import { Address } from "viem"; +import { useAccount } from "wagmi"; +import { useLiquidityHubSwapContext } from "./context"; + +export const QUOTE_REFETCH_INTERVAL = 20_000; + +export const useParaswapMinAmountOut = () => { + const { + state: { slippage }, + } = useLiquidityHubSwapContext(); + const optimalRate = useOptimalRate().data; + return useMemo(() => { + return getMinAmountOut(slippage, optimalRate?.destAmount || "0"); + }, [optimalRate?.destAmount, slippage]); +}; + +export function useLiquidityHubQuote() { + const queryClient = useQueryClient(); + const { chainId, address: account } = useAccount(); + const { + state: { inToken, outToken, liquidityHubDisabled, slippage }, + sdk, + parsedInputAmount, + } = useLiquidityHubSwapContext(); + const dexMinAmountOut = useParaswapMinAmountOut(); + + const inTokenAddress = resolveNativeTokenAddress(inToken?.address); + const outTokenAddress = outToken?.address; + // Check if the swap is wrap or unwrap only + const { isUnwrapOnly, isWrapOnly } = useWrapOrUnwrapOnly( + inTokenAddress, + outTokenAddress + ); + + const enabled = Boolean( + !liquidityHubDisabled && + chainId && + inTokenAddress && + outTokenAddress && + Number(parsedInputAmount) > 0 && + !isUnwrapOnly && + !isWrapOnly && + account + ); + + const queryKey = useMemo( + () => [ + "quote", + inTokenAddress, + outTokenAddress, + parsedInputAmount, + slippage, + dexMinAmountOut, + ], + [ + inTokenAddress, + parsedInputAmount, + slippage, + outTokenAddress, + dexMinAmountOut, + ] + ); + + const getQuote = useCallback( + ({ signal }: { signal: AbortSignal }) => { + if (!inTokenAddress || !outTokenAddress || !parsedInputAmount) { + return Promise.reject(new Error("Invalid input")); + } + return sdk.getQuote({ + fromToken: inTokenAddress, + toToken: outTokenAddress, + inAmount: parsedInputAmount, + dexMinAmountOut, + account, + slippage, + signal, + }); + }, + [ + sdk, + inTokenAddress, + outTokenAddress, + parsedInputAmount, + account, + slippage, + dexMinAmountOut, + ] + ); + + const query = useQuery({ + queryKey, + queryFn: getQuote, + enabled, + refetchOnWindowFocus: false, + staleTime: Infinity, + gcTime: 0, + retry: 2, + refetchInterval: QUOTE_REFETCH_INTERVAL, + placeholderData: (prev) => prev, + }); + + return useMemo(() => { + return { + // We return the result of getQuote, plus a function to get + // the last fetched quote in react-query cache + ...query, + getLatestQuote: () => + queryClient.ensureQueryData({ + queryKey, + queryFn: getQuote, + }), + }; + }, [query, queryClient, queryKey, getQuote]); +} + +export const useOptimalRate = () => { + const { + parsedInputAmount, + state: { inToken, outToken }, + } = useLiquidityHubSwapContext(); + return useParaswapQuote({ + inToken: inToken?.address || "", + outToken: outToken?.address || "", + inAmount: parsedInputAmount, + refetchInterval: QUOTE_REFETCH_INTERVAL, + }); +}; + +export const useLiquidityHubInputError = () => { + const { + state: { inToken, inputAmount }, + } = useLiquidityHubSwapContext(); + return useInputError({ + inputAmount, + inToken, + }); +}; + +const useNetwork = () => { + const { chainId } = useAccount(); + + return useMemo(() => { + return Object.values(networks).find((network) => network.id === chainId); + }, [chainId]); +}; + +const useNativeOrWrapped = (address?: string) => { + const callback = useNativeOrWrappedAddressCallback(); + return useMemo(() => callback(address), [address]); +}; + +const useNativeOrWrappedAddressCallback = () => { + const network = useNetwork(); + return useCallback( + (address?: string) => { + return isNativeAddress(address) ? network?.wToken.address : address; + }, + [network] + ); +}; + +export const useLiquidityHubApproval = () => { + const { + parsedInputAmount, + state: { inToken }, + } = useLiquidityHubSwapContext(); + const tokenAddress = useNativeOrWrapped(inToken?.address); + return useGetRequiresApproval( + permit2Address, + tokenAddress, + parsedInputAmount + ); +}; + +export const useParaswapApproval = () => { + const optimalRate = useOptimalRate().data; + + const tokenAddress = useNativeOrWrapped(optimalRate?.srcToken); + return useGetRequiresApproval( + optimalRate?.tokenTransferProxy as Address, + tokenAddress, + optimalRate?.srcAmount + ); +}; diff --git a/src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx b/src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx index db8df57..80d8237 100644 --- a/src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx +++ b/src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx @@ -1,60 +1,53 @@ -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogTitle, -} from '@/components/ui/dialog' -import { LiquidityProvider, SwapSteps, Token } from '@/types' -import { Card } from '@/components/ui/card' -import { SwapFlow, SwapStep, SwapStatus } from '@orbs-network/swap-ui' -import { useMemo } from 'react' -import { DataDetails } from '@/components/ui/data-details' +import { Button } from "@/components/ui/button"; +import { SwapSteps } from "@/types"; +import { Card } from "@/components/ui/card"; +import { SwapStep, SwapStatus } from "@orbs-network/swap-ui"; +import { useCallback, useMemo } from "react"; +import { DataDetails } from "@/components/ui/data-details"; +import BN from "bignumber.js"; import { format, - fromBigNumber, + getErrorMessage, getLiquidityProviderName, getSteps, - resolveNativeTokenAddress, - toBigNumber, - useGetRequiresApproval, -} from '@/lib' -import { useAccount } from 'wagmi' -import { Address } from 'viem' - -export type SwapConfirmationDialogProps = { - inToken: Token - outToken: Token - isOpen: boolean - onClose: () => void - confirmSwap: () => void - swapStatus?: SwapStatus - currentStep?: SwapSteps - signature?: string - gasAmountOut?: string - liquidityProvider: LiquidityProvider - inAmount?: number - inAmountUsd?: string - outAmount?: number - outAmountUsd?: string - allowancePermitAddress: string -} + promiseWithTimeout, + toExactAmount, + useParaswapBuildTxCallback, + usePriceUsd, + wagmiConfig, + waitForConfirmations, +} from "@/lib"; +import { useAccount } from "wagmi"; +import { Address } from "viem"; +import { estimateGas, sendTransaction, signTypedData } from "wagmi/actions"; +import { approveAllowance } from "@/lib/approveAllowance"; +import { useMutation } from "@tanstack/react-query"; +import { useLiquidityHubSwapContext } from "./context"; +import { + useLiquidityHubApproval, + useLiquidityHubQuote, + useOptimalRate, + useParaswapApproval, +} from "./hooks"; +import { + SwapConfirmationDialog, + SwapProgressState, + useSwapProgress, +} from "../swap-confirmation-dialog"; +import { wrapToken } from "@/lib/wrapToken"; +import { _TypedDataEncoder } from "@ethersproject/hash"; +import { permit2Address, Quote } from "@orbs-network/liquidity-hub-sdk"; +import { TransactionParams } from "@paraswap/sdk"; +import { toast } from "sonner"; +import { useToExactAmount } from "../hooks"; // Construct steps for swap to display in UI -const useSteps = ( - liquidityProvider: LiquidityProvider, - requiresApproval: boolean, - inToken?: Token, - signature?: string -) => { +const useSteps = (steps?: number[]) => { + const { + state: { inToken, signature }, + } = useLiquidityHubSwapContext(); return useMemo((): SwapStep[] => { - if (!inToken) return [] - - const steps = getSteps({ - noWrap: liquidityProvider === 'paraswap', - inTokenAddress: inToken.address, - requiresApproval, - }) + if (!steps || !inToken) return []; return steps.map((step) => { if (step === SwapSteps.Wrap) { @@ -63,7 +56,7 @@ const useSteps = ( title: `Wrap ${inToken.symbol}`, description: `Wrap ${inToken.symbol}`, image: inToken?.logoUrl, - } + }; } if (step === SwapSteps.Approve) { return { @@ -71,7 +64,7 @@ const useSteps = ( title: `Approve ${inToken.symbol}`, description: `Approve ${inToken.symbol}`, image: inToken?.logoUrl, - } + }; } return { id: SwapSteps.Swap, @@ -79,119 +72,515 @@ const useSteps = ( description: `Swap ${inToken.symbol}`, image: inToken?.logoUrl, timeout: signature ? 60_000 : 40_000, + }; + }); + }, [inToken, steps, signature]); +}; + +export function LiquidityHubConfirmationDialog({ + isOpen, + onClose: _onClose, +}: { + isOpen: boolean; + onClose: (swapStatus?: SwapStatus) => void; +}) { + const { + state: { inputAmount, inToken, outToken, isLiquidityHubTrade }, + } = useLiquidityHubSwapContext(); + + const { + state: progressState, + resetState: resetProgressState, + updateState: updateProgressState, + } = useSwapProgress(); + + const parsedSteps = useSteps(progressState.steps); + + const { mutate: swapWithLiquidityHub } = + useLiquidityHubSwapCallback(updateProgressState); + const { mutate: swapWithParaswap } = + useParaswapSwapCallback(updateProgressState); + + const paraswapApproval = useParaswapApproval(); + const liquidityHubApproval = useLiquidityHubApproval(); + + const approvalLoading = isLiquidityHubTrade + ? liquidityHubApproval.approvalLoading + : paraswapApproval.approvalLoading; + const onSubmit = useCallback(async () => { + if (!isLiquidityHubTrade) { + console.log("Proceeding with Liquidity Hub"); + swapWithParaswap(); + } else { + swapWithLiquidityHub(); + } + }, [ + isLiquidityHubTrade, + swapWithLiquidityHub, + swapWithParaswap, + updateProgressState, + ]); + + const onClose = useCallback(() => { + _onClose(progressState.swapStatus); + setTimeout(() => { + if (progressState.currentStep) { + resetProgressState(); } - }) - }, [inToken, liquidityProvider, requiresApproval, signature]) + }, 500); + }, [ + _onClose, + progressState.swapStatus, + progressState.currentStep, + resetProgressState, + ]); + + const usdValues = useUSDValues(); + + const optimalRate = useOptimalRate().data; + + const quote = useLiquidityHubQuote().data; + + const result = isLiquidityHubTrade + ? quote?.outAmount + : optimalRate?.destAmount; + + const outAmount = useToExactAmount(result, outToken?.decimals); + + return ( + + } + details={
} + /> + } + /> + ); } -export function SwapConfirmationDialog({ - inToken, - outToken, - isOpen, - onClose, - confirmSwap, - swapStatus, - currentStep, - signature, - gasAmountOut, - liquidityProvider, - inAmount, - inAmountUsd, - outAmount, - outAmountUsd, - allowancePermitAddress, -}: SwapConfirmationDialogProps) { - const { address } = useAccount() +const useUSDValues = () => { + const { + state: { inToken, outToken, inputAmount, isLiquidityHubTrade }, + } = useLiquidityHubSwapContext(); + const srcUSD = usePriceUsd(inToken?.address).data; + const destUSD = usePriceUsd(outToken?.address).data; + const optimalRate = useOptimalRate().data; + const quote = useLiquidityHubQuote().data; + + return useMemo(() => { + if (!isLiquidityHubTrade) { + return { + srcUSD: optimalRate?.srcUSD, + destUSD: optimalRate?.destUSD, + }; + } + return { + srcUSD: BN(inputAmount) + .multipliedBy(srcUSD || 0) + .toString(), + destUSD: BN(toExactAmount(quote?.outAmount, outToken?.decimals)) + .multipliedBy(destUSD || 0) + .toString(), + }; + }, [ + isLiquidityHubTrade, + srcUSD, + destUSD, + optimalRate, + quote, + inputAmount, + outToken?.decimals, + ]); +}; + +const SubmitSwapButton = ({ + onClick, + approvalLoading, +}: { + onClick: () => void; + approvalLoading: boolean; +}) => { + const { + state: { inToken, outToken }, + } = useLiquidityHubSwapContext(); + return ( + + ); +}; + +const Details = () => { + const optimalRate = useOptimalRate().data; + const quote = useLiquidityHubQuote().data; + const address = useAccount().address; + const { + state: { outToken, isLiquidityHubTrade }, + } = useLiquidityHubSwapContext(); + const outAmountUsd = optimalRate?.destUSD; + const outAmount = isLiquidityHubTrade + ? quote?.outAmount + : optimalRate?.destAmount; const gasPrice = useMemo(() => { - if (!outAmountUsd || !gasAmountOut) return 0 - const gas = fromBigNumber(gasAmountOut, outToken.decimals) - const usd = Number(outAmountUsd) / Number(outAmount) - return Number(gas) * usd - }, [outAmountUsd, gasAmountOut, outToken.decimals, outAmount]) - - const { requiresApproval, approvalLoading } = useGetRequiresApproval( - allowancePermitAddress as Address, - resolveNativeTokenAddress(inToken?.address), - toBigNumber(inAmount || 0, inToken?.decimals) - ) - - const steps = useSteps( - liquidityProvider, - requiresApproval, - inToken, - signature - ) + const gasAmountOut = isLiquidityHubTrade + ? toExactAmount(quote?.gasAmountOut, outToken?.decimals) + : BN(optimalRate?.gasCost || 0) + .dividedBy(1e18) + .toString(); + + if (!outAmountUsd || !gasAmountOut || !outToken) return 0; + const gas = toExactAmount(gasAmountOut, outToken.decimals); + const usd = Number(outAmountUsd) / Number(outAmount); + return Number(gas) * usd; + }, [outAmountUsd, outToken, outAmount, quote, isLiquidityHubTrade]); return ( - - - Swap - -
-
- - } - swapStatus={swapStatus} - successContent={} - failedContent={} - inToken={{ - symbol: inToken.symbol, - logo: inToken.logoUrl, - }} - outToken={{ - symbol: outToken.symbol, - logo: outToken.logoUrl, - }} - /> -
- - {!swapStatus && address && ( - <> - -
- -
-
- -
- -
-
- - - - )} +
+ +
+ +
+
+ +
+
- -
- ) -} \ No newline at end of file + + + ); +}; + +export const useParaswapSwapCallback = ( + updateSwapProgressState: (value: Partial) => void +) => { + const buildParaswapTxCallback = useParaswapBuildTxCallback(); + const optimalRate = useOptimalRate().data; + const { + state: { slippage, inToken }, + } = useLiquidityHubSwapContext(); + const requiresApproval = useParaswapApproval().requiresApproval; + + const { address } = useAccount(); + + return useMutation({ + mutationFn: async () => { + if (!address) { + throw new Error("Wallet not connected"); + } + + if (!inToken) { + throw new Error("Input token not found"); + } + + if (!optimalRate) { + throw new Error("No optimal rate found"); + } + + try { + updateSwapProgressState({ swapStatus: SwapStatus.LOADING }); + + const steps = getSteps({ + inTokenAddress: inToken.address, + requiresApproval, + noWrap: true, + }); + updateSwapProgressState({ steps }); + if (requiresApproval) { + updateSwapProgressState({ currentStep: SwapSteps.Approve }); + await approveAllowance( + address, + optimalRate.srcToken, + optimalRate.tokenTransferProxy as Address + ); + } + + updateSwapProgressState({ currentStep: SwapSteps.Swap }); + + let txPayload: unknown | null = null; + + try { + const txData = await buildParaswapTxCallback(optimalRate, slippage); + + txPayload = { + account: txData.from as Address, + to: txData.to as Address, + data: txData.data as `0x${string}`, + gasPrice: BigInt(txData.gasPrice), + gas: txData.gas ? BigInt(txData.gas) : undefined, + value: BigInt(txData.value), + }; + } catch (error) { + // Handle error in UI + console.error(error); + + updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); + } + + if (!txPayload) { + updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); + + throw new Error("Failed to build transaction"); + } + + console.log("Swapping..."); + + await estimateGas(wagmiConfig, txPayload); + + const txHash = await sendTransaction(wagmiConfig, txPayload); + + await waitForConfirmations(txHash, 1, 20); + + updateSwapProgressState({ swapStatus: SwapStatus.SUCCESS }); + + return txHash; + } catch (error) { + console.error(error); + updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); + toast.error("An error occurred while swapping"); + throw error; + } + }, + }); +}; + +// Analytics events are optional for integration but are useful for your business insights +type AnalyticsEvents = { + onRequest: () => void; + onSuccess: (result?: string) => void; + onFailure: (error: string) => void; +}; + +async function wrapTokenCallback( + quote: Quote, + analyticsEvents: AnalyticsEvents +) { + try { + console.log("Wrapping token..."); + analyticsEvents.onRequest(); + + // Perform the deposit contract function + const txHash = await wrapToken(quote.user, quote.inAmount); + + // Check for confirmations for a maximum of 20 seconds + await waitForConfirmations(txHash, 1, 20); + console.log("Token wrapped"); + analyticsEvents.onSuccess(); + + return txHash; + } catch (error) { + analyticsEvents.onFailure( + getErrorMessage(error, "An error occurred while wrapping your token") + ); + throw error; + } +} + +async function approveCallback( + account: string, + inToken: string, + analyticsEvents: AnalyticsEvents +) { + try { + analyticsEvents.onRequest(); + // Perform the approve contract function + const txHash = await approveAllowance(account, inToken, permit2Address); + + analyticsEvents.onSuccess(txHash); + return txHash; + } catch (error) { + analyticsEvents.onFailure( + getErrorMessage(error, "An error occurred while approving the allowance") + ); + throw error; + } +} + +async function signTransaction(quote: Quote, analyticsEvents: AnalyticsEvents) { + // Encode the payload to get signature + const { permitData } = quote; + const populated = await _TypedDataEncoder.resolveNames( + permitData.domain, + permitData.types, + permitData.values, + async (name: string) => name + ); + const payload = _TypedDataEncoder.getPayload( + populated.domain, + permitData.types, + populated.value + ); + + try { + console.log("Signing transaction..."); + analyticsEvents.onRequest(); + + // Sign transaction and get signature + const signature = await promiseWithTimeout( + signTypedData(wagmiConfig, payload), + 40_000 + ); + + console.log("Transaction signed"); + analyticsEvents.onSuccess(signature); + + return signature; + } catch (error) { + console.error(error); + + analyticsEvents.onFailure( + getErrorMessage(error, "An error occurred while getting the signature") + ); + throw error; + } +} + +export function useLiquidityHubSwapCallback( + updateSwapProgressState: (partial: Partial) => void +) { + const { + sdk: liquidityHub, + state: { inToken, slippage }, + updateState, + } = useLiquidityHubSwapContext(); + const buildParaswapTxCallback = useParaswapBuildTxCallback(); + const optimalRate = useOptimalRate().data; + const { getLatestQuote, data: quote } = useLiquidityHubQuote(); + const requiresApproval = useLiquidityHubApproval().requiresApproval; + + const inTokenAddress = inToken?.address; + + return useMutation({ + mutationFn: async () => { + // Fetch latest quote just before swap + if (!inTokenAddress) { + throw new Error("In token address is not set"); + } + + if (!quote || !optimalRate) { + throw new Error("Quote or optimal rate is not set"); + } + // Set swap status for UI + updateSwapProgressState({ swapStatus: SwapStatus.LOADING }); + + try { + // Check if the inToken needs approval for allowance + + // Get the steps required for swap e.g. [Wrap, Approve, Swap] + const steps = getSteps({ + inTokenAddress, + requiresApproval, + }); + + updateSwapProgressState({ steps }); + + // If the inToken needs to be wrapped then wrap + if (steps.includes(SwapSteps.Wrap)) { + updateSwapProgressState({ currentStep: SwapSteps.Wrap }); + await wrapTokenCallback(quote, { + onRequest: liquidityHub.analytics.onWrapRequest, + onSuccess: liquidityHub.analytics.onWrapSuccess, + onFailure: liquidityHub.analytics.onWrapFailure, + }); + } + + // If an appropriate allowance for inToken has not been approved + // then get user to approve + if (steps.includes(SwapSteps.Approve)) { + updateSwapProgressState({ currentStep: SwapSteps.Approve }); + await approveCallback(quote.user, quote.inToken, { + onRequest: liquidityHub.analytics.onApprovalRequest, + onSuccess: liquidityHub.analytics.onApprovalSuccess, + onFailure: liquidityHub.analytics.onApprovalFailed, + }); + } + + // Fetch the latest quote again after the approval + const latestQuote = await getLatestQuote(); + updateState({ acceptedQuote: latestQuote }); + + // Set the current step to swap + updateSwapProgressState({ currentStep: SwapSteps.Swap }); + + // Sign the transaction for the swap + const signature = await signTransaction(latestQuote, { + onRequest: liquidityHub.analytics.onSignatureRequest, + onSuccess: (signature) => + liquidityHub.analytics.onSignatureSuccess(signature || ""), + onFailure: liquidityHub.analytics.onSignatureFailed, + }); + updateState({ signature }); + + // Pass the liquidity provider txData if possible + let paraswapTxData: TransactionParams | undefined; + + try { + paraswapTxData = await buildParaswapTxCallback(optimalRate, slippage); + } catch (error) { + console.error(error); + } + + console.log("Swapping..."); + // Call Liquidity Hub sdk swap and wait for transaction hash + const txHash = await liquidityHub.swap( + latestQuote, + signature as string, + { + data: paraswapTxData?.data, + to: paraswapTxData?.to, + } + ); + + if (!txHash) { + throw new Error("Swap failed"); + } + + // Fetch the successful transaction details + await liquidityHub.getTransactionDetails(txHash, latestQuote); + + console.log("Swapped"); + updateSwapProgressState({ swapStatus: SwapStatus.SUCCESS }); + } catch (error) { + updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); + + throw error; + } + }, + }); +} diff --git a/src/trade/liquidity-hub/liquidity-hub-swap.tsx b/src/trade/liquidity-hub/liquidity-hub-swap.tsx index 34acd18..f9780d3 100644 --- a/src/trade/liquidity-hub/liquidity-hub-swap.tsx +++ b/src/trade/liquidity-hub/liquidity-hub-swap.tsx @@ -1,292 +1,73 @@ import { TokenCard } from "@/components/tokens/token-card"; import { SwitchButton } from "@/components/ui/switch-button"; -import { Token } from "@/types"; -import { - createContext, - ReactNode, - useCallback, - useContext, - useMemo, - useReducer, -} from "react"; +import { useCallback, useMemo, useState } from "react"; import { useAccount } from "wagmi"; -import { SwapDetails } from "../../components/swap-details"; import { Button } from "@/components/ui/button"; - -import { useMutation } from "@tanstack/react-query"; -import { estimateGas, sendTransaction, signTypedData } from "wagmi/actions"; import { _TypedDataEncoder } from "@ethersproject/hash"; import { SwapStatus } from "@orbs-network/swap-ui"; -import { SwapSteps } from "@/types"; -import { OptimalRate, TransactionParams } from "@paraswap/sdk"; -import { approveAllowance } from "@/lib/approveAllowance"; -import { getRequiresApproval } from "@/lib/getRequiresApproval"; -import { wrapToken } from "@/lib/wrapToken"; - -import { - constructSDK, - permit2Address, - Quote, - LiquidityHubSDK, -} from "@orbs-network/liquidity-hub-sdk"; +import { Token } from "@/types"; import { - useDefaultTokens, ErrorCodes, - fromBigNumber, - useTokensWithBalances, + format, + getLiquidityProviderName, getMinAmountOut, - useParaswapQuote, getQuoteErrorMessage, - fromBigNumberToStr, - getErrorMessage, - resolveNativeTokenAddress, - useWrapOrUnwrapOnly, - wagmiConfig, - waitForConfirmations, - promiseWithTimeout, - getSteps, - useParaswapBuildTxCallback, + toExactAmount, } from "@/lib"; import "../style.css"; import { useConnectModal } from "@rainbow-me/rainbowkit"; -import { toast } from "sonner"; import { SettingsIcon } from "lucide-react"; -import BN from "bignumber.js"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; -import { useInputError } from "../../lib/useHandleInputError"; -import { useToRawAmount } from "../hooks"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; +import BN from "bignumber.js"; import { - SwapConfirmationDialog, - SwapProgressState, - useSwapProgress, -} from "../swap-confirmation-dialog"; -import { Address } from "viem"; - -const initialState: State = { - inToken: null, - outToken: null, - inputAmount: "", - acceptedQuote: undefined, - liquidityHubDisabled: false, - slippage: 0.5, - forceLiquidityHub: false, - showConfirmation: false, -}; - -interface State { - inToken: Token | null; - outToken: Token | null; - inputAmount: string; - acceptedQuote: Quote | undefined; - liquidityHubDisabled: boolean; - slippage: number; - forceLiquidityHub: boolean; - showConfirmation: boolean; -} - -type Action = { type: "UPDATE"; payload: Partial } | { type: "RESET" }; - -const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "UPDATE": - return { ...state, ...action.payload }; - case "RESET": - return initialState; - default: - return state; - } -}; - -interface ContextType { - state: State; - updateState: (payload: Partial) => void; - resetState: () => void; - sdk: LiquidityHubSDK; - parsedInputAmount?: string; -} - -const Context = createContext({} as ContextType); -const useSwapContext = () => { - return useContext(Context); -}; - -export const SwapProvider = ({ children }: { children: ReactNode }) => { - const [state, dispatch] = useReducer(reducer, initialState); - const { chainId } = useAccount(); - const { tokensWithBalances, refetch: refetchBalances } = - useTokensWithBalances(); - const parsedInputAmount = useToRawAmount( - state.inputAmount, - state.inToken?.decimals - ); - - const updateState = useCallback( - (payload: Partial) => { - dispatch({ type: "UPDATE", payload }); - }, - [dispatch] - ); - - const resetState = useCallback(() => { - dispatch({ type: "RESET" }); - refetchBalances(); - }, [dispatch, refetchBalances]); - - const sdk = useMemo( - () => constructSDK({ partner: "widget", chainId }), - [chainId] - ); - - useDefaultTokens({ - inToken: state.inToken, - outToken: state.outToken, - tokensWithBalances, - setInToken: (token) => updateState({ inToken: token }), - setOutToken: (token) => updateState({ outToken: token }), - }); - - return ( - - {children} - - ); -}; - -export const QUOTE_REFETCH_INTERVAL = 20_000; - -// ------------ Fetches quote using Liquidity Hub sdk ------------ // - -export function useLiquidityHubQuote(dexMinAmountOut?: string) { - const queryClient = useQueryClient(); - const { chainId, address: account } = useAccount(); + LiquidityHubSwapProvider, + useLiquidityHubSwapContext, +} from "./context"; +import { useToExactAmount } from "../hooks"; +import { LiquidityHubConfirmationDialog } from "./liquidity-hub-confirmation-dialog"; +import { + useLiquidityHubInputError, + useLiquidityHubQuote, + useOptimalRate, + useParaswapMinAmountOut, +} from "./hooks"; +import { DataDetails } from "@/components/ui/data-details"; +import { Separator } from "@radix-ui/react-dropdown-menu"; + +export const useIsLiquidityHubTrade = () => { const { - state: { inToken, outToken, liquidityHubDisabled, slippage }, - sdk, - parsedInputAmount, - } = useSwapContext(); - const inTokenAddress = resolveNativeTokenAddress(inToken?.address); - const outTokenAddress = outToken?.address; - // Check if the swap is wrap or unwrap only - const { isUnwrapOnly, isWrapOnly } = useWrapOrUnwrapOnly( - inTokenAddress, - outTokenAddress - ); + state: { liquidityHubDisabled }, + } = useLiquidityHubSwapContext(); + const liquidityHubQuote = useLiquidityHubQuote().data; + const paraswapMinAmountOut = useParaswapMinAmountOut(); - const enabled = Boolean( - !liquidityHubDisabled && - chainId && - inTokenAddress && - outTokenAddress && - Number(parsedInputAmount) > 0 && - !isUnwrapOnly && - !isWrapOnly - ); - - const queryKey = useMemo( - () => [ - "quote", - inTokenAddress, - outTokenAddress, - parsedInputAmount, - slippage, - ], - [inTokenAddress, parsedInputAmount, slippage, outTokenAddress] - ); - - const getQuote = useCallback( - ({ signal }: { signal: AbortSignal }) => { - if (!inTokenAddress || !outTokenAddress || !parsedInputAmount) { - return Promise.reject(new Error("Invalid input")); - } - return sdk.getQuote({ - fromToken: inTokenAddress, - toToken: outTokenAddress, - inAmount: parsedInputAmount, - dexMinAmountOut, - account, - slippage, - signal, - }); - }, - [sdk, inTokenAddress, outTokenAddress, parsedInputAmount, account, slippage] - ); - - const query = useQuery({ - queryKey, - queryFn: getQuote, - enabled, - refetchOnWindowFocus: false, - staleTime: Infinity, - gcTime: 0, - retry: 2, - refetchInterval: QUOTE_REFETCH_INTERVAL, - }); + console.log(liquidityHubQuote?.minAmountOut, paraswapMinAmountOut); return useMemo(() => { - return { - // We return the result of getQuote, plus a function to get - // the last fetched quote in react-query cache - ...query, - getLatestQuote: () => - queryClient.ensureQueryData({ - queryKey, - queryFn: getQuote, - }), - }; - }, [query, queryClient, queryKey, getQuote]); -} + // Choose between liquidity hub and dex swap based on the min amount out + if (liquidityHubDisabled) return false; -const useOptimalRate = () => { - const { - parsedInputAmount, - state: { inToken, outToken }, - } = useSwapContext(); - return useParaswapQuote({ - inToken: inToken?.address || "", - outToken: outToken?.address || "", - inAmount: parsedInputAmount, - }); + return BN(liquidityHubQuote?.minAmountOut || 0).gt( + paraswapMinAmountOut || 0 + ); + }, [ + liquidityHubDisabled, + liquidityHubQuote?.minAmountOut, + paraswapMinAmountOut, + ]); }; -// ------------ Swap ----------- // - function SwapPanel() { - const { tokensWithBalances } = useTokensWithBalances(); const { - state: { - inToken, - outToken, - inputAmount, - slippage, - acceptedQuote, - forceLiquidityHub, - liquidityHubDisabled, - }, + state: { inToken, outToken }, updateState, - resetState, - parsedInputAmount, - } = useSwapContext(); - - const inputError = useInputError({ - inputAmount, - inToken, - }); + } = useLiquidityHubSwapContext(); // Handle Token Switch const handleSwitch = useCallback(() => { @@ -297,625 +78,268 @@ function SwapPanel() { }); }, [inToken, outToken, updateState]); - // Handle Swap Confirmation Dialog Close - const onSwapConfirmClose = useCallback(() => { - resetState(); - }, [resetState]); - - /* --------- Quote ---------- */ - // The entered input amount has to be converted to a big int string - // to be used for getting quotes - - const { data: optimalRate, isLoading: optimalRateLoading } = useOptimalRate(); - - const paraswapMinAmountOut = getMinAmountOut( - slippage, - optimalRate?.destAmount || "0" - ); - - // Fetch Liquidity Hub Quote - const { - data: _quote, - getLatestQuote, - error: quoteError, - } = useLiquidityHubQuote(); - - const liquidityHubQuote = acceptedQuote || _quote; - - /* --------- End Quote ---------- */ - - /* --------- Swap ---------- */ - const isLiquidityHubTrade = useMemo(() => { - // Choose between liquidity hub and dex swap based on the min amount out - if ( - forceLiquidityHub || - (!liquidityHubDisabled && - BN(liquidityHubQuote?.minAmountOut || 0).gt(paraswapMinAmountOut || 0)) - ) { - return true; - } - return false; - }, [ - forceLiquidityHub, - liquidityHubDisabled, - liquidityHubQuote?.minAmountOut, - paraswapMinAmountOut, - ]); - - const swapWithParaswap = useCallback(async () => { - if (!optimalRate) return; - try { - await paraswapSwapCallback({ - optimalRate, - slippage, - setCurrentStep, - setSwapStatus, - onFailure: resetSwap, - }); - } catch (error) { - console.error(error); - toast.error(getErrorMessage(error, "An error occurred while swapping")); - } - }, [optimalRate, paraswapSwapCallback, resetSwap, slippage]); - - const swapWithLiquidityHub = useCallback(async () => { - if (!optimalRate) { - toast.error("An unknown error occurred"); - return; - } - - try { - await liquidityHubSwapCallback({ - inTokenAddress: inToken!.address, - getQuote: getLatestQuote, - onAcceptQuote, - setSwapStatus, - setCurrentStep, - onFailure: resetSwap, - setSignature, - slippage, - optimalRate, - }); - } catch (error) { - // If the liquidity hub swap fails, need to set the flag to prevent further attempts, and proceed with the dex swap - // stop quoting from liquidity hub - // start new flow with dex swap - console.error(error); - console.log("Liquidity Hub Swap failed, proceeding with ParaSwap..."); - setLiquidityHubDisabled(true); - swapWithParaswap(); - } - }, [ - optimalRate, - liquidityHubSwapCallback, - inToken, - getLatestQuote, - onAcceptQuote, - resetSwap, - slippage, - swapWithParaswap, - ]); - - const confirmSwap = useCallback(async () => { - if (isLiquidityHubTrade) { - console.log("Proceeding with Liquidity Hub"); - swapWithLiquidityHub(); - } else { - console.log("Proceeding with ParaSwap"); - setLiquidityHubDisabled(true); - swapWithParaswap(); - } - }, [isLiquidityHubTrade, swapWithLiquidityHub, swapWithParaswap]); - /* --------- End Swap ---------- */ - - const destAmount = optimalRate?.destAmount - ? fromBigNumberToStr(optimalRate.destAmount, outToken?.decimals) - : ""; - const outAmount = useMemo(() => first, [second]); - - const { openConnectModal } = useConnectModal(); return (
-
- - - - - -
-
- -
- setSlippage(e.target.valueAsNumber)} - value={slippage} - step={0.1} - className="text-right w-16 [&::-webkit-inner-spin-button]:appearance-none p-2 h-7" - /> -
%
-
-
-
- - - setForceLiquidityHub(checked) - } - checked={forceLiquidityHub} - /> -
-
-
-
-
+
- +
- - {account.address && account.isConnected && outToken && inToken ? ( - <> - - - - - ) : ( - - )} - - {quoteError && ( -
- {getQuoteErrorMessage(quoteError.message)} -
- )} - - + + +
); } -export const Swap = () => { +const Settings = () => { + const { + state: { slippage }, + updateState, + } = useLiquidityHubSwapContext(); return ( - - - +
+ + + + + +
+
+ +
+ + updateState({ slippage: e.target.valueAsNumber }) + } + value={slippage} + step={0.1} + className="text-right w-16 [&::-webkit-inner-spin-button]:appearance-none p-2 h-7" + /> +
%
+
+
+
+
+
+
); }; -const LiquididyHubConfirmationDialog = () => { +const ConfirmationModal = () => { + const account = useAccount().address; + const openConnectModal = useConnectModal().openConnectModal; + const { data: liquidityHubQuote } = useLiquidityHubQuote(); + const { data: optimalRate, isLoading: optimalRateLoading } = useOptimalRate(); + const inputError = useLiquidityHubInputError(); + const isLiquidityHubTrade = useIsLiquidityHubTrade(); const { - state: { showConfirmation, inToken, outToken, inputAmount }, + state: { inputAmount }, updateState, - } = useSwapContext(); + resetState, + } = useLiquidityHubSwapContext(); + const [isOpen, setIsOpen] = useState(false); + + const { text, enabled } = useMemo(() => { + if (inputError === ErrorCodes.InsufficientBalance) { + return { + text: "Insufficient balance", + }; + } + if (inputError === ErrorCodes.EnterAmount) { + return { + text: "Enter amount", + }; + } + if (inputAmount && optimalRateLoading) { + return { + text: "Fetching quote...", + }; + } - const { - state: { currentStep, swapStatus }, - updateState: updateSwapProgressState, - } = useSwapProgress(); - const { mutateAsync: liquidityHubSwapCallback } = useLiquidityHubSwapCallback( - updateSwapProgressState - ); - const { mutateAsync: paraswapSwapCallback } = useParaswapSwapCallback(); + if (!optimalRate && inputAmount) { + return { + text: "Fetching rate...", + }; + } - const onClose = useCallback(() => { - updateState({ showConfirmation: false }); - }, [updateState]); + return { + text: "Swap", + enabled: true, + }; + }, [ + inputError, + inputAmount, + liquidityHubQuote, + optimalRate, + optimalRateLoading, + ]); - return ( - + const onOpen = useCallback(() => { + setIsOpen(true); + updateState({ isLiquidityHubTrade }); + }, [isLiquidityHubTrade]); + + const onClose = useCallback( + (status?: SwapStatus) => { + setIsOpen(false); + updateState({ isLiquidityHubTrade: false }); + if (status === SwapStatus.SUCCESS) { + updateState({inputAmount: ''}); + } + }, + [resetState] ); -}; - -// Analytics events are optional for integration but are useful for your business insights -type AnalyticsEvents = { - onRequest: () => void; - onSuccess: (result?: string) => void; - onFailure: (error: string) => void; -}; -async function wrapTokenCallback( - quote: Quote, - analyticsEvents: AnalyticsEvents -) { - try { - console.log("Wrapping token..."); - analyticsEvents.onRequest(); - - // Perform the deposit contract function - const txHash = await wrapToken(quote.user, quote.inAmount); - - // Check for confirmations for a maximum of 20 seconds - await waitForConfirmations(txHash, 1, 20); - console.log("Token wrapped"); - analyticsEvents.onSuccess(); - - return txHash; - } catch (error) { - analyticsEvents.onFailure( - getErrorMessage(error, "An error occurred while wrapping your token") + if (!account) { + return ( + ); - throw error; } -} -async function approveCallback( - account: string, - inToken: string, - analyticsEvents: AnalyticsEvents -) { - try { - analyticsEvents.onRequest(); - // Perform the approve contract function - const txHash = await approveAllowance(account, inToken, permit2Address); - - analyticsEvents.onSuccess(txHash); - return txHash; - } catch (error) { - analyticsEvents.onFailure( - getErrorMessage(error, "An error occurred while approving the allowance") - ); - throw error; - } -} - -async function signTransaction(quote: Quote, analyticsEvents: AnalyticsEvents) { - // Encode the payload to get signature - const { permitData } = quote; - const populated = await _TypedDataEncoder.resolveNames( - permitData.domain, - permitData.types, - permitData.values, - async (name: string) => name - ); - const payload = _TypedDataEncoder.getPayload( - populated.domain, - permitData.types, - populated.value + return ( + <> + + + ); +}; - try { - console.log("Signing transaction..."); - analyticsEvents.onRequest(); - - // Sign transaction and get signature - const signature = await promiseWithTimeout( - signTypedData(wagmiConfig, payload), - 40_000 - ); - - console.log("Transaction signed"); - analyticsEvents.onSuccess(signature); - - return signature; - } catch (error) { - console.error(error); - - analyticsEvents.onFailure( - getErrorMessage(error, "An error occurred while getting the signature") - ); - throw error; - } -} - -export const useParaswapSwapCallback = ( - updateSwapProgressState: (value: Partial) => void -) => { - const buildParaswapTxCallback = useParaswapBuildTxCallback(); +export function SwapDetails() { + const isLiquidityHubTrade = useIsLiquidityHubTrade(); const optimalRate = useOptimalRate().data; + const account = useAccount().address; const { - state: { slippage }, - } = useSwapContext(); - const { address } = useAccount(); - - return useMutation({ - mutationFn: async ({ - onSuccess, - onFailure, - }: { - onSuccess?: () => void; - onFailure?: () => void; - }) => { - if (!address) { - throw new Error("Wallet not connected"); - } - - if (!optimalRate) { - throw new Error("No optimal rate found"); - } - - try { - updateSwapProgressState({ swapStatus: SwapStatus.LOADING }); - - // Check if the inToken needs approval for allowance - const requiresApproval = await getRequiresApproval( - optimalRate.tokenTransferProxy, - resolveNativeTokenAddress(optimalRate.srcToken), - optimalRate.srcAmount, - address - ); - - if (requiresApproval) { - updateSwapProgressState({ currentStep: SwapSteps.Approve }); - await approveAllowance( - address, - optimalRate.srcToken, - optimalRate.tokenTransferProxy as Address - ); - } - - updateSwapProgressState({ currentStep: SwapSteps.Swap }); - - let txPayload: unknown | null = null; + state: { outToken, inToken }, + } = useLiquidityHubSwapContext(); + const minAmountOut = useParaswapMinAmountOut(); + const inPriceUsd = useMemo(() => { + if (!optimalRate) return 0; + const amount = toExactAmount(optimalRate.srcAmount, inToken?.decimals); + return Number(optimalRate.srcUSD) / Number(amount); + }, [optimalRate, inToken]); + + const minOutAmount = useToExactAmount(minAmountOut, outToken?.decimals); + const outAmount = useToExactAmount( + optimalRate?.destAmount, + outToken?.decimals + ); - try { - const txData = await buildParaswapTxCallback(optimalRate, slippage); + const outPriceUsd = useMemo(() => { + if (!optimalRate) return 0; + const amount = toExactAmount(optimalRate.destAmount, outToken?.decimals); + return Number(optimalRate.destUSD) / Number(amount); + }, [optimalRate, outToken]); - txPayload = { - account: txData.from as Address, - to: txData.to as Address, - data: txData.data as `0x${string}`, - gasPrice: BigInt(txData.gasPrice), - gas: txData.gas ? BigInt(txData.gas) : undefined, - value: BigInt(txData.value), - }; - } catch (error) { - // Handle error in UI - console.error(error); - if (onFailure) onFailure(); - updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); - } + if (!inToken || !outToken || !account || !optimalRate) return null; - if (!txPayload) { - if (onFailure) onFailure(); - updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); + const rate = inPriceUsd / outPriceUsd; - throw new Error("Failed to build transaction"); - } + let data: Record = { + Rate: `1 ${inToken.symbol} ≈ ${format.crypto(rate)} ${outToken.symbol}`, + }; - console.log("Swapping..."); + data = { + ...data, + "Est. Received": `${format.crypto(Number(outAmount))} ${outToken.symbol}`, + "Min. Received": `${format.crypto(Number(minOutAmount))} ${ + outToken.symbol + }`, + "Routing source": getLiquidityProviderName(isLiquidityHubTrade), + }; - await estimateGas(wagmiConfig, txPayload); + return ( +
+ + +
+
Recepient
+
{format.address(account)}
+
+
+ ); +} - const txHash = await sendTransaction(wagmiConfig, txPayload); +const InTokenCard = () => { + const { + state: { inputAmount, inToken }, + updateState, + } = useLiquidityHubSwapContext(); + const optimalRate = useOptimalRate().data; + const onSelectToken = useCallback( + (inToken: Token) => { + updateState({ inToken }); + }, + [updateState] + ); - await waitForConfirmations(txHash, 1, 20); + const onValueChange = useCallback( + (inputAmount: string) => { + updateState({ inputAmount }); + }, + [updateState] + ); - if (onSuccess) onSuccess(); + const inputError = useLiquidityHubInputError(); - updateSwapProgressState({ swapStatus: SwapStatus.SUCCESS }); + return ( + + ); +}; - return txHash; - } catch (error) { - console.error(error); - if (onFailure) onFailure(); - updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); +const OutTokenCard = () => { + const { data: optimalRate, isLoading: optimalRateLoading } = useOptimalRate(); + const { + state: { outToken }, + updateState, + } = useLiquidityHubSwapContext(); + const destAmount = useToExactAmount( + optimalRate?.destAmount, + outToken?.decimals + ); - throw error; - } + const onSelectToken = useCallback( + (outToken: Token) => { + updateState({ outToken }); }, - }); -}; + [updateState] + ); -export function useLiquidityHubSwapCallback( - updateSwapProgressState: (partial: Partial) => void -) { - const { sdk: liquidityHub, state:{inToken, slippage} } = useSwapContext(); - const buildParaswapTxCallback = useParaswapBuildTxCallback(); - const account = useAccount(); - const optimalRate = useOptimalRate().data; + return ( + + ); +}; - const inTokenAddress = inToken?.address - - - return useMutation({ - mutationFn: async ({ - getQuote, - onAcceptQuote, - setSwapStatus, - setCurrentStep, - onSuccess, - onFailure, - setSignature, - }: { - getQuote: () => Promise; - onAcceptQuote: (quote: Quote) => void; - setSwapStatus: (status?: SwapStatus) => void; - setCurrentStep: (step: SwapSteps) => void; - setSignature: (signature: string) => void; - onSuccess?: () => void; - onFailure?: () => void; - }) => { - // Fetch latest quote just before swap - const quote = await getQuote(); - // Set swap status for UI - setSwapStatus(SwapStatus.LOADING); - - try { - // Check if the inToken needs approval for allowance - const requiresApproval = await getRequiresApproval( - permit2Address, - resolveNativeTokenAddress(inTokenAddress), - quote.inAmount, - account.address as string - ); - - // Get the steps required for swap e.g. [Wrap, Approve, Swap] - const steps = getSteps({ - inTokenAddress, - requiresApproval, - }); - - // If the inToken needs to be wrapped then wrap - if (steps.includes(SwapSteps.Wrap)) { - setCurrentStep(SwapSteps.Wrap); - await wrapTokenCallback(quote, { - onRequest: liquidityHub.analytics.onWrapRequest, - onSuccess: liquidityHub.analytics.onWrapSuccess, - onFailure: liquidityHub.analytics.onWrapFailure, - }); - } - - // If an appropriate allowance for inToken has not been approved - // then get user to approve - if (steps.includes(SwapSteps.Approve)) { - setCurrentStep(SwapSteps.Approve); - await approveCallback(quote.user, quote.inToken, { - onRequest: liquidityHub.analytics.onApprovalRequest, - onSuccess: liquidityHub.analytics.onApprovalSuccess, - onFailure: liquidityHub.analytics.onApprovalFailed, - }); - } - - // Fetch the latest quote again after the approval - const latestQuote = await getQuote(); - onAcceptQuote(latestQuote); - - // Set the current step to swap - setCurrentStep(SwapSteps.Swap); - - // Sign the transaction for the swap - const signature = await signTransaction(latestQuote, { - onRequest: liquidityHub.analytics.onSignatureRequest, - onSuccess: (signature) => - liquidityHub.analytics.onSignatureSuccess(signature || ""), - onFailure: liquidityHub.analytics.onSignatureFailed, - }); - setSignature(signature); - - // Pass the liquidity provider txData if possible - let paraswapTxData: TransactionParams | undefined; - - try { - paraswapTxData = await buildParaswapTxCallback(optimalRate, slippage); - } catch (error) { - console.error(error); - } - - console.log("Swapping..."); - // Call Liquidity Hub sdk swap and wait for transaction hash - const txHash = await liquidityHub.swap( - latestQuote, - signature as string, - { - data: paraswapTxData?.data, - to: paraswapTxData?.to, - } - ); - - if (!txHash) { - throw new Error("Swap failed"); - } - - // Fetch the successful transaction details - await liquidityHub.getTransactionDetails(txHash, latestQuote); - - console.log("Swapped"); - setSwapStatus(SwapStatus.SUCCESS); - if (onSuccess) onSuccess(); - } catch (error) { - setSwapStatus(SwapStatus.FAILED); - if (onFailure) onFailure(); - - throw error; - } - }, - }); -} +export const Swap = () => { + return ( + + + + ); +}; diff --git a/src/trade/twap/components/limit-price-input.tsx b/src/trade/twap/components/limit-price-input.tsx index 3d20d76..0a17a62 100644 --- a/src/trade/twap/components/limit-price-input.tsx +++ b/src/trade/twap/components/limit-price-input.tsx @@ -68,7 +68,6 @@ const useOnPercentSelect = () => { }; export function LimitPriceInput() { - const tokens = useTokensWithBalances().tokensWithBalances; const { state: { values, updateState }, isMarketOrder, @@ -104,7 +103,7 @@ export function LimitPriceInput() { return marketPriceUI; }, [customTradePrice, marketPriceUI, isTradePriceInverted]); - const usd = usePriceUsd(networks.poly.id, selectedToken?.address).data; + const usd = usePriceUsd(selectedToken?.address).data; const amountUsd = !inputValue || !usd ? "" : BN(inputValue).multipliedBy(usd).toNumber(); @@ -152,7 +151,6 @@ export function LimitPriceInput() {
diff --git a/src/trade/twap/components/src-chunk-size.tsx b/src/trade/twap/components/src-chunk-size.tsx index c8819c1..7866f23 100644 --- a/src/trade/twap/components/src-chunk-size.tsx +++ b/src/trade/twap/components/src-chunk-size.tsx @@ -1,4 +1,5 @@ -import { fromBigNumberToStr, networks, usePriceUsd } from "@/lib"; +import { usePriceUsd } from "@/lib"; +import { useToExactAmount } from "@/trade/hooks"; import { useDerivedTwapSwapData } from "../hooks"; import { useTwapContext } from "../twap-context"; @@ -8,8 +9,8 @@ export function SrcChunkSize() { values: { inToken }, } = state; const { srcChunkAmount } = useDerivedTwapSwapData(); - const usd = usePriceUsd(networks.poly.id, inToken?.address).data; - const srcChunkAmountUi = fromBigNumberToStr( + const usd = usePriceUsd(inToken?.address).data; + const srcChunkAmountUi = useToExactAmount( srcChunkAmount || "0", inToken?.decimals ); diff --git a/src/trade/twap/hooks.ts b/src/trade/twap/hooks.ts index 4c25cd6..d2c323f 100644 --- a/src/trade/twap/hooks.ts +++ b/src/trade/twap/hooks.ts @@ -1,7 +1,5 @@ import { - networks, - toBigInt, - toBigNumber, + toRawAmount, useInputError, useParaswapQuote, usePriceUsd, @@ -9,7 +7,7 @@ import { import { useMemo } from "react"; import { useTwapContext } from "./twap-context"; import BN from "bignumber.js"; -import { useToExactAmount } from "../hooks"; +import { useToExactAmount, useToRawAmount } from "../hooks"; import { MAX_FILL_DELAY_DAYS, MIN_DURATION_MINUTES, @@ -30,7 +28,6 @@ export const useDerivedTwapSwapData = () => { const price = useTradePrice(); const { data: oneSrcTokenUsd } = usePriceUsd( - networks.poly.id, inToken?.address ); @@ -61,7 +58,7 @@ export const useOptimalRate = () => { return useParaswapQuote({ inToken: inToken?.address || "", outToken: outToken?.address || "", - inAmount: toBigNumber("1", inToken?.decimals), + inAmount: useToRawAmount("1", inToken?.decimals), }); }; @@ -82,7 +79,7 @@ export const useTradePrice = () => { result = 1 / Number(customTradePrice); } - return toBigInt(result, outToken?.decimals).toString(); + return toRawAmount(result.toString(), outToken?.decimals).toString(); }, [ isMarketOrder, outToken?.decimals, @@ -94,7 +91,7 @@ export const useTradePrice = () => { export const useInTokenUsd = () => { const { inToken, typedAmount } = useTwapContext().state.values; - const usd = usePriceUsd(networks.poly.id, inToken?.address).data; + const usd = usePriceUsd(inToken?.address).data; return !usd || !typedAmount ? "" @@ -104,7 +101,7 @@ export const useInTokenUsd = () => { export const useOutTokenUsd = () => { const { destTokenAmount } = useDerivedTwapSwapData(); const { outToken } = useTwapContext().state.values; - const usd = usePriceUsd(networks.poly.id, outToken?.address).data; + const usd = usePriceUsd(outToken?.address).data; const amount = useToExactAmount(destTokenAmount, outToken?.decimals); return !usd || !amount ? "" : BN(amount).multipliedBy(usd).toString(); diff --git a/src/trade/twap/twap-confirmation-dialog.tsx b/src/trade/twap/twap-confirmation-dialog.tsx index b447c0c..bf162a4 100644 --- a/src/trade/twap/twap-confirmation-dialog.tsx +++ b/src/trade/twap/twap-confirmation-dialog.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button"; import { SwapSteps } from "@/types"; import { useCallback, useMemo } from "react"; -import { SwapConfirmationDialog, useSwapProgress } from "../swap-confirmation-dialog"; +import { SwapConfirmationDialog, SwapProgressState, useSwapProgress } from "../swap-confirmation-dialog"; import { useDerivedTwapSwapData, useInputLabels, @@ -36,7 +36,6 @@ import { simulateContract, writeContract, } from "wagmi/actions"; -import { SwapState } from "../use-swap-progress"; import { useWaitForNewOrderCallback } from "./orders/use-orders-query"; @@ -243,7 +242,7 @@ const useApproveCallback = () => { }; function useCreateOrder( - updateState: (state: Partial) => void, + updateState: (state: Partial) => void, requiresApproval: boolean ) { const { diff --git a/src/trade/twap/twap.tsx b/src/trade/twap/twap.tsx index af220c2..5b14fb7 100644 --- a/src/trade/twap/twap.tsx +++ b/src/trade/twap/twap.tsx @@ -68,8 +68,6 @@ export function Panel() { useTwapStateActions(); const { destTokenAmount } = useDerivedTwapSwapData(); const destAmount = useToExactAmount(destTokenAmount, outToken?.decimals); - const inTokenBalance = useTokenBalance(inToken?.address); - const outTokenBalance = useTokenBalance(outToken?.address); const [showSwapConfirmationModal, setShowSwapConfirmationModal] = useState(false); @@ -128,9 +126,7 @@ export function Panel() { label={inputLabel} amount={typedAmount} amountUsd={inAmountUsd} - balance={inTokenBalance} selectedToken={inToken || defaultTokens[0]} - tokens={tokensWithBalances || {}} onSelectToken={setInToken} onValueChange={setInputAmount} inputError={inputError} @@ -142,9 +138,7 @@ export function Panel() { label={outputLabel} amount={destAmount ?? ""} amountUsd={outAmountUsd} - balance={outTokenBalance} selectedToken={outToken || defaultTokens[1]} - tokens={tokensWithBalances || {}} onSelectToken={setOutToken} isAmountEditable={false} amountLoading={amountLoading}