From f6779578d5d4cf4d9e80c6e4934b52d18dab0ac1 Mon Sep 17 00:00:00 2001 From: denis-orbs Date: Sun, 20 Oct 2024 13:44:17 +0300 Subject: [PATCH 1/3] a --- package.json | 2 +- src/components/swap-details.tsx | 6 +- src/lib/useParaswap.ts | 99 --- src/lib/useWrapOrUnwrapOnly.ts | 4 +- src/lib/utils.ts | 22 +- .../liquidity-hub/liquidity-hub-swap.tsx | 819 +++++++++++++++--- .../liquidity-hub/useLiquidityHubQuote.ts | 76 -- src/trade/liquidity-hub/useLiquidityHubSDK.ts | 9 - .../useLiquidityHubSwapCallback.ts | 235 ----- src/trade/swap-confirmation-dialog.tsx | 58 ++ src/trade/twap/twap-confirmation-dialog.tsx | 7 +- src/trade/use-swap-state.ts | 58 -- src/types.ts | 1 - yarn.lock | 8 +- 14 files changed, 754 insertions(+), 650 deletions(-) delete mode 100644 src/trade/liquidity-hub/useLiquidityHubQuote.ts delete mode 100644 src/trade/liquidity-hub/useLiquidityHubSDK.ts delete mode 100644 src/trade/liquidity-hub/useLiquidityHubSwapCallback.ts delete mode 100644 src/trade/use-swap-state.ts diff --git a/package.json b/package.json index 813b919..0f0f878 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@ethersproject/hash": "^5.7.0", - "@orbs-network/liquidity-hub-sdk": "^1.0.40", + "@orbs-network/liquidity-hub-sdk": "^1.0.41", "@orbs-network/swap-ui": "^0.0.14", "@orbs-network/twap-sdk": "^2.0.38", "@paraswap/sdk": "^6.10.0", diff --git a/src/components/swap-details.tsx b/src/components/swap-details.tsx index c0de6a3..f5245b1 100644 --- a/src/components/swap-details.tsx +++ b/src/components/swap-details.tsx @@ -11,7 +11,7 @@ export type SwapDetailsProps = { outToken: Token | null account?: string minAmountOut?: string - liquidityProvider: LiquidityProvider + isLiquidityHubTrade: boolean } export function SwapDetails({ @@ -20,7 +20,7 @@ export function SwapDetails({ account, minAmountOut, optimalRate, - liquidityProvider, + isLiquidityHubTrade, }: SwapDetailsProps) { const inPriceUsd = useMemo(() => { if (!optimalRate) return 0 @@ -48,7 +48,7 @@ export function SwapDetails({ ...data, 'Est. Received': `${format.crypto(Number(outAmount))} ${outToken.symbol}`, 'Min. Received': `${format.crypto(minOutAmount)} ${outToken.symbol}`, - 'Routing source': getLiquidityProviderName(liquidityProvider), + 'Routing source': getLiquidityProviderName(isLiquidityHubTrade), } return ( diff --git a/src/lib/useParaswap.ts b/src/lib/useParaswap.ts index ce84f49..8043cc5 100644 --- a/src/lib/useParaswap.ts +++ b/src/lib/useParaswap.ts @@ -103,102 +103,3 @@ export const useParaswapBuildTxCallback = () => { ) } -export const useParaswapSwapCallback = () => { - const buildParaswapTxCallback = useParaswapBuildTxCallback() - const { address } = useAccount() - - return useMutation({ - mutationFn: async ({ - optimalRate, - slippage, - setSwapStatus, - setCurrentStep, - onSuccess, - onFailure, - }: { - optimalRate: OptimalRate - slippage: number - setSwapStatus: (status?: SwapStatus) => void - setCurrentStep: (step: SwapSteps) => void - onSuccess?: () => void - onFailure?: () => void - }) => { - if (!address) { - throw new Error('Wallet not connected') - } - - try { - setSwapStatus(SwapStatus.LOADING) - - // Check if the inToken needs approval for allowance - const requiresApproval = await getRequiresApproval( - optimalRate.tokenTransferProxy, - resolveNativeTokenAddress(optimalRate.srcToken), - optimalRate.srcAmount, - address - ) - - if (requiresApproval) { - setCurrentStep(SwapSteps.Approve) - await approveAllowance( - address, - optimalRate.srcToken, - optimalRate.tokenTransferProxy as Address - ) - } - - setCurrentStep(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) - if (onFailure) onFailure() - setSwapStatus(SwapStatus.FAILED) - } - - if (!txPayload) { - if (onFailure) onFailure() - setSwapStatus(SwapStatus.FAILED) - - throw new Error('Failed to build transaction') - } - - console.log('Swapping...') - // Use estimate gas to simulate send transaction - // if any error occurs, it will be caught and handled - // without spending any gas - await estimateGas(wagmiConfig, txPayload) - - const txHash = await sendTransaction(wagmiConfig, txPayload) - - await waitForConfirmations(txHash, 1, 20) - - if (onSuccess) onSuccess() - - setSwapStatus(SwapStatus.SUCCESS) - console.log('Swapped') - - return txHash - } catch (error) { - console.error(error) - if (onFailure) onFailure() - setSwapStatus(SwapStatus.FAILED) - - throw error - } - }, - }) -} diff --git a/src/lib/useWrapOrUnwrapOnly.ts b/src/lib/useWrapOrUnwrapOnly.ts index 7ad02d3..e340a25 100644 --- a/src/lib/useWrapOrUnwrapOnly.ts +++ b/src/lib/useWrapOrUnwrapOnly.ts @@ -3,8 +3,8 @@ import { networks } from './networks' import { eqIgnoreCase, isNativeAddress } from './utils' export function useWrapOrUnwrapOnly( - fromTokenAddress: string, - toTokenAddress: string + fromTokenAddress?: string, + toTokenAddress?: string ) { // Evaluates whether tokens are to be wrapped/unwrapped only return useMemo(() => { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 58798b7..6c0b42d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,7 +2,7 @@ import { zeroAddress } from "@orbs-network/liquidity-hub-sdk"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import { wagmiConfig } from "@/lib/wagmi-config"; -import { LiquidityProvider, SwapSteps } from "@/types"; +import { SwapSteps } from "@/types"; import { getTransactionConfirmations } from "wagmi/actions"; import { networks } from "./networks"; import BN from "bignumber.js"; @@ -19,7 +19,7 @@ export const toBigInt = (amount: string | number, decimals?: number) => { export const toExactAmount = ( amount?: string, decimals?: number, - decimalScale? : number + decimalScale?: number ) => { if (!decimals || !amount) return ""; const percision = BN(10).pow(decimals || 0); @@ -34,12 +34,6 @@ export const toRawAmount = (amount?: string, decimals?: number) => { return BN(amount).times(BN(10).pow(decimals)).decimalPlaces(0).toFixed(); }; -export const toBigNumber = (amount: string | number, decimals?: number) => { - if (amount === "") return "0"; - - return toBigInt(amount, decimals).toString(); -}; - export const fromBigNumberToStr = ( amount: bigint | string, decimals?: number @@ -213,15 +207,11 @@ export function getErrorMessage( return errorMessage; } -export function getLiquidityProviderName(provider: LiquidityProvider) { - switch (provider) { - case "paraswap": - return "ParaSwap"; - case "liquidityhub": - return "Liquidity Hub"; - default: - return "Unknown"; +export function getLiquidityProviderName(isLiquidityHubTrade: boolean) { + if (isLiquidityHubTrade) { + return "Liquidity Hub"; } + return "ParaSwap"; } export const makeElipsisAddress = (address?: string, padding = 6): string => { diff --git a/src/trade/liquidity-hub/liquidity-hub-swap.tsx b/src/trade/liquidity-hub/liquidity-hub-swap.tsx index fa76047..34acd18 100644 --- a/src/trade/liquidity-hub/liquidity-hub-swap.tsx +++ b/src/trade/liquidity-hub/liquidity-hub-swap.tsx @@ -1,171 +1,349 @@ -import { TokenCard } from '@/components/tokens/token-card' -import { SwitchButton } from '@/components/ui/switch-button' -import { SwapSteps, Token } from '@/types' -import { useCallback, useMemo, useState } from 'react' -import { SwapStatus } from '@orbs-network/swap-ui' -import { useAccount } from 'wagmi' -import { SwapDetails } from '../../components/swap-details' -import { SwapConfirmationDialog } from './liquidity-hub-confirmation-dialog' -import { useLiquidityHubQuote } from './useLiquidityHubQuote' -import { Button } from '@/components/ui/button' -import { useLiquidityHubSwapCallback } from './useLiquidityHubSwapCallback' -import { permit2Address, Quote } from '@orbs-network/liquidity-hub-sdk' +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 { 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 { useDefaultTokens, ErrorCodes, fromBigNumber, - toBigNumber, useTokensWithBalances, getMinAmountOut, useParaswapQuote, getQuoteErrorMessage, - useParaswapSwapCallback, - toBigInt, fromBigNumberToStr, getErrorMessage, -} from '@/lib' -import '../style.css' -import { useConnectModal } from '@rainbow-me/rainbowkit' -import { toast } from 'sonner' -import { SettingsIcon } from 'lucide-react' + resolveNativeTokenAddress, + useWrapOrUnwrapOnly, + wagmiConfig, + waitForConfirmations, + promiseWithTimeout, + getSteps, + useParaswapBuildTxCallback, +} 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' +} 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 { + 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; +} -export function Swap() { +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 [inToken, setInToken] = useState(null) - const [outToken, setOutToken] = useState(null) - const [inputAmount, setInputAmount] = useState('') - const [acceptedQuote, setAcceptedQuote] = useState() - const [liquidityHubDisabled, setLiquidityHubDisabled] = useState(false) - const [currentStep, setCurrentStep] = useState( - undefined - ) - const [swapStatus, setSwapStatus] = useState( - undefined - ) - const [swapConfirmOpen, setSwapConfirmOpen] = useState(false) - const [signature, setSignature] = useState(undefined) - const [forceLiquidityHub, setForceLiquidityHub] = useState(false) - const [slippage, setSlippage] = useState(0.5) - - // Get wagmi account - const account = useAccount() - - // Set Initial Tokens - const defaultTokens = useDefaultTokens({ - inToken, - outToken, + 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, - setOutToken, - }) + 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(); + 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 + ); + + 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, + }); + + 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]); +} + +const useOptimalRate = () => { + const { + parsedInputAmount, + state: { inToken, outToken }, + } = useSwapContext(); + return useParaswapQuote({ + inToken: inToken?.address || "", + outToken: outToken?.address || "", + inAmount: parsedInputAmount, + }); +}; + +// ------------ Swap ----------- // + +function SwapPanel() { + const { tokensWithBalances } = useTokensWithBalances(); + const { + state: { + inToken, + outToken, + inputAmount, + slippage, + acceptedQuote, + forceLiquidityHub, + liquidityHubDisabled, + }, + updateState, + resetState, + parsedInputAmount, + } = useSwapContext(); - // Handle Amount Input Error const inputError = useInputError({ inputAmount, inToken, - }) + }); // Handle Token Switch const handleSwitch = useCallback(() => { - setInToken(outToken) - setOutToken(inToken) - setInputAmount('') - }, [inToken, outToken]) - - const resetSwap = useCallback(() => { - setAcceptedQuote(undefined) - setInputAmount('') - setCurrentStep(undefined) - setSignature(undefined) - setSwapStatus(undefined) - setLiquidityHubDisabled(false) - refetchBalances() - }, [refetchBalances]) + updateState({ + inToken: outToken, + outToken: inToken, + inputAmount: "", + }); + }, [inToken, outToken, updateState]); // Handle Swap Confirmation Dialog Close const onSwapConfirmClose = useCallback(() => { - setSwapConfirmOpen(false) - resetSwap() - }, [resetSwap]) + resetState(); + }, [resetState]); /* --------- Quote ---------- */ // The entered input amount has to be converted to a big int string // to be used for getting quotes - const inputAmountAsBigNumber = toBigNumber(inputAmount, inToken?.decimals) - const { data: optimalRate, isLoading: optimalRateLoading } = useParaswapQuote( - { - inToken: inToken?.address || '', - outToken: outToken?.address || '', - inAmount: inputAmountAsBigNumber, - } - ) + const { data: optimalRate, isLoading: optimalRateLoading } = useOptimalRate(); const paraswapMinAmountOut = getMinAmountOut( slippage, - optimalRate?.destAmount || '0' - ) + optimalRate?.destAmount || "0" + ); // Fetch Liquidity Hub Quote const { data: _quote, getLatestQuote, error: quoteError, - } = useLiquidityHubQuote( - { - fromToken: inToken?.address || '', - toToken: outToken?.address || '', - inAmount: inputAmountAsBigNumber, - slippage, - account: account.address, - dexMinAmountOut: paraswapMinAmountOut, - }, - liquidityHubDisabled - ) + } = useLiquidityHubQuote(); - const liquidityHubQuote = acceptedQuote || _quote + const liquidityHubQuote = acceptedQuote || _quote; /* --------- End Quote ---------- */ /* --------- Swap ---------- */ - const liquidityProvider = useMemo(() => { + const isLiquidityHubTrade = useMemo(() => { // Choose between liquidity hub and dex swap based on the min amount out if ( forceLiquidityHub || (!liquidityHubDisabled && - toBigInt(liquidityHubQuote?.minAmountOut || 0) > - BigInt(paraswapMinAmountOut || 0)) + BN(liquidityHubQuote?.minAmountOut || 0).gt(paraswapMinAmountOut || 0)) ) { - return 'liquidityhub' + return true; } - - return 'paraswap' + return false; }, [ forceLiquidityHub, liquidityHubDisabled, liquidityHubQuote?.minAmountOut, paraswapMinAmountOut, - ]) - - const onAcceptQuote = useCallback((quote?: Quote) => { - setAcceptedQuote(quote) - }, []) - const { mutateAsync: liquidityHubSwapCallback } = - useLiquidityHubSwapCallback() - const { mutateAsync: paraswapSwapCallback } = useParaswapSwapCallback() + ]); const swapWithParaswap = useCallback(async () => { - if (!optimalRate) return + if (!optimalRate) return; try { await paraswapSwapCallback({ optimalRate, @@ -173,17 +351,17 @@ export function Swap() { setCurrentStep, setSwapStatus, onFailure: resetSwap, - }) + }); } catch (error) { - console.error(error) - toast.error(getErrorMessage(error, 'An error occurred while swapping')) + console.error(error); + toast.error(getErrorMessage(error, "An error occurred while swapping")); } - }, [optimalRate, paraswapSwapCallback, resetSwap, slippage]) + }, [optimalRate, paraswapSwapCallback, resetSwap, slippage]); const swapWithLiquidityHub = useCallback(async () => { if (!optimalRate) { - toast.error('An unknown error occurred') - return + toast.error("An unknown error occurred"); + return; } try { @@ -197,15 +375,15 @@ export function Swap() { 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() + console.error(error); + console.log("Liquidity Hub Swap failed, proceeding with ParaSwap..."); + setLiquidityHubDisabled(true); + swapWithParaswap(); } }, [ optimalRate, @@ -216,25 +394,26 @@ export function Swap() { resetSwap, slippage, swapWithParaswap, - ]) + ]); const confirmSwap = useCallback(async () => { - if (liquidityProvider === 'liquidityhub') { - console.log('Proceeding with Liquidity Hub') - swapWithLiquidityHub() + if (isLiquidityHubTrade) { + console.log("Proceeding with Liquidity Hub"); + swapWithLiquidityHub(); } else { - console.log('Proceeding with ParaSwap') - setLiquidityHubDisabled(true) - swapWithParaswap() + console.log("Proceeding with ParaSwap"); + setLiquidityHubDisabled(true); + swapWithParaswap(); } - }, [liquidityProvider, swapWithLiquidityHub, swapWithParaswap]) + }, [isLiquidityHubTrade, swapWithLiquidityHub, swapWithParaswap]); /* --------- End Swap ---------- */ const destAmount = optimalRate?.destAmount ? fromBigNumberToStr(optimalRate.destAmount, outToken?.decimals) - : '' + : ""; + const outAmount = useMemo(() => first, [second]); - const { openConnectModal } = useConnectModal() + const { openConnectModal } = useConnectModal(); return (
@@ -264,7 +443,9 @@ export function Swap() { setForceLiquidityHub(checked)} + onCheckedChange={(checked: any) => + setForceLiquidityHub(checked) + } checked={forceLiquidityHub} />
@@ -294,7 +475,7 @@ export function Swap() {
{inputError === ErrorCodes.InsufficientBalance - ? 'Insufficient balance' + ? "Insufficient balance" : inputAmount && !liquidityHubQuote - ? 'Fetching quote...' + ? "Fetching quote..." : !optimalRate && inputAmount - ? 'No liquidity' - : 'Swap'} + ? "No liquidity" + : "Swap"} ) : ( @@ -378,9 +558,364 @@ export function Swap() { outToken={outToken} minAmountOut={paraswapMinAmountOut} account={account.address} - liquidityProvider={liquidityProvider} + isLiquidityHubTrade={isLiquidityHubTrade} /> - ) + ); +} + +export const Swap = () => { + return ( + + + + ); +}; + +const LiquididyHubConfirmationDialog = () => { + const { + state: { showConfirmation, inToken, outToken, inputAmount }, + updateState, + } = useSwapContext(); + + const { + state: { currentStep, swapStatus }, + updateState: updateSwapProgressState, + } = useSwapProgress(); + const { mutateAsync: liquidityHubSwapCallback } = useLiquidityHubSwapCallback( + updateSwapProgressState + ); + const { mutateAsync: paraswapSwapCallback } = useParaswapSwapCallback(); + + const onClose = useCallback(() => { + updateState({ showConfirmation: false }); + }, [updateState]); + + return ( + + ); +}; + +// 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 const useParaswapSwapCallback = ( + updateSwapProgressState: (value: Partial) => void +) => { + const buildParaswapTxCallback = useParaswapBuildTxCallback(); + const optimalRate = useOptimalRate().data; + 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; + + 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); + if (onFailure) onFailure(); + updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); + } + + if (!txPayload) { + if (onFailure) onFailure(); + 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); + + if (onSuccess) onSuccess(); + + updateSwapProgressState({ swapStatus: SwapStatus.SUCCESS }); + + return txHash; + } catch (error) { + console.error(error); + if (onFailure) onFailure(); + updateSwapProgressState({ swapStatus: SwapStatus.FAILED }); + + throw error; + } + }, + }); +}; + +export function useLiquidityHubSwapCallback( + updateSwapProgressState: (partial: Partial) => void +) { + const { sdk: liquidityHub, state:{inToken, slippage} } = useSwapContext(); + const buildParaswapTxCallback = useParaswapBuildTxCallback(); + const account = useAccount(); + const optimalRate = useOptimalRate().data; + + 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; + } + }, + }); } diff --git a/src/trade/liquidity-hub/useLiquidityHubQuote.ts b/src/trade/liquidity-hub/useLiquidityHubQuote.ts deleted file mode 100644 index a8ada63..0000000 --- a/src/trade/liquidity-hub/useLiquidityHubQuote.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { QuoteArgs } from '@orbs-network/liquidity-hub-sdk' -import { useQuery, useQueryClient } from '@tanstack/react-query' -import { useLiquidityHubSDK } from './useLiquidityHubSDK' -import { useAccount } from 'wagmi' -import { useCallback, useMemo } from 'react' -import { useWrapOrUnwrapOnly, resolveNativeTokenAddress } from '@/lib' - -export const QUOTE_REFETCH_INTERVAL = 20_000 - -// Fetches quote using Liquidity Hub sdk - -export function useLiquidityHubQuote(args: QuoteArgs, disabled?: boolean) { - const liquidityHub = useLiquidityHubSDK() - const queryClient = useQueryClient() - const { chainId } = useAccount() - - // Check if the swap is wrap or unwrap only - const { isUnwrapOnly, isWrapOnly } = useWrapOrUnwrapOnly( - args.fromToken, - args.toToken - ) - - // Flag to determine whether to getQuote - const enabled = Boolean( - !disabled && - chainId && - args.fromToken && - args.toToken && - Number(args.inAmount) > 0 && - !isUnwrapOnly && - !isWrapOnly - ) - - const queryKey = useMemo( - () => ['quote', args.fromToken, args.toToken, args.inAmount, args.slippage], - [args.fromToken, args.inAmount, args.slippage, args.toToken] - ) - - // Callback to call Liquidity Hub sdk getQuote - const getQuote = useCallback( - ({ signal }: { signal: AbortSignal }) => { - const payload: QuoteArgs = { - ...args, - fromToken: resolveNativeTokenAddress(args.fromToken)!, - } - // The abort signal is optional - return liquidityHub.getQuote({ ...payload, signal }) - }, - [liquidityHub, args] - ) - - // result from getQuote - const query = useQuery({ - queryKey, - queryFn: getQuote, - enabled, - refetchOnWindowFocus: false, - staleTime: Infinity, - gcTime: 0, - retry: 2, - refetchInterval: QUOTE_REFETCH_INTERVAL, - }) - - 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]) -} diff --git a/src/trade/liquidity-hub/useLiquidityHubSDK.ts b/src/trade/liquidity-hub/useLiquidityHubSDK.ts deleted file mode 100644 index bebb6f2..0000000 --- a/src/trade/liquidity-hub/useLiquidityHubSDK.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { constructSDK } from '@orbs-network/liquidity-hub-sdk' -import { useMemo } from 'react' -import { useAccount } from 'wagmi' - -// Initialise Liquidity Hub sdk and provide in a React Hook -export function useLiquidityHubSDK() { - const { chainId } = useAccount() - return useMemo(() => constructSDK({ partner: 'widget', chainId }), [chainId]) -} diff --git a/src/trade/liquidity-hub/useLiquidityHubSwapCallback.ts b/src/trade/liquidity-hub/useLiquidityHubSwapCallback.ts deleted file mode 100644 index 3a8901a..0000000 --- a/src/trade/liquidity-hub/useLiquidityHubSwapCallback.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { useMutation } from '@tanstack/react-query' -import { signTypedData } from 'wagmi/actions' -import { _TypedDataEncoder } from '@ethersproject/hash' -import { permit2Address, Quote } from '@orbs-network/liquidity-hub-sdk' -import { SwapStatus } from '@orbs-network/swap-ui' -import { useLiquidityHubSDK } from './useLiquidityHubSDK' -import { SwapSteps } from '@/types' -import { - wagmiConfig, - waitForConfirmations, - promiseWithTimeout, - getSteps, - getErrorMessage, - useParaswapBuildTxCallback, - resolveNativeTokenAddress, -} from '@/lib' -import { OptimalRate, TransactionParams } from '@paraswap/sdk' -import { approveAllowance } from '@/lib/approveAllowance' -import { getRequiresApproval } from '@/lib/getRequiresApproval' -import { useAccount } from 'wagmi' -import { wrapToken } from '@/lib/wrapToken' - -// 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() { - const liquidityHub = useLiquidityHubSDK() - const buildParaswapTxCallback = useParaswapBuildTxCallback() - const account = useAccount() - - return useMutation({ - mutationFn: async ({ - inTokenAddress, - optimalRate, - slippage, - getQuote, - onAcceptQuote, - setSwapStatus, - setCurrentStep, - onSuccess, - onFailure, - setSignature, - }: { - inTokenAddress: string - slippage: number - optimalRate: OptimalRate - 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 - } - }, - }) -} diff --git a/src/trade/swap-confirmation-dialog.tsx b/src/trade/swap-confirmation-dialog.tsx index f28cca6..db6e012 100644 --- a/src/trade/swap-confirmation-dialog.tsx +++ b/src/trade/swap-confirmation-dialog.tsx @@ -9,6 +9,7 @@ import { SwapFlow, SwapStatus, SwapStep } from "@orbs-network/swap-ui"; import { createContext, ReactNode, useContext } from "react"; import { format } from "@/lib"; import { Skeleton } from "@/components/ui/skeleton"; +import { useCallback, useReducer } from "react"; export type Props = { inToken: Token | null; @@ -139,3 +140,60 @@ const StepsLoader = () => { }; SwapConfirmationDialog.Main = Main; + + + +export type SwapProgressState = { + swapStatus?: SwapStatus; + currentStep?: number; + shouldUnwrap?: boolean; + txHash?: string; + steps?: number[]; + error?: string; +}; + + +type Action = + | { type: "UPDATE_STATE"; payload: Partial } + | { type: "RESET" }; + +function reducer( + state: SwapProgressState, + action: Action, + initialState: SwapProgressState +): SwapProgressState { + switch (action.type) { + case "UPDATE_STATE": + return { ...state, ...action.payload }; + case "RESET": + return initialState; + default: + return state; + } +} + +const initialState = {} as SwapProgressState; + +export const useSwapProgress = () => { + const [state, dispatch] = useReducer( + (state: SwapProgressState, action: Action) => reducer(state, action, initialState), + initialState + ); + + const updateState = useCallback( + (payload: Partial) => { + dispatch({ type: "UPDATE_STATE", payload }); + }, + [dispatch] + ); + + const resetState = useCallback(() => { + dispatch({ type: "RESET" }); + }, [dispatch]); + + return { + state, + updateState, + resetState, + }; +}; diff --git a/src/trade/twap/twap-confirmation-dialog.tsx b/src/trade/twap/twap-confirmation-dialog.tsx index 39cbbc3..b447c0c 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 } from "../swap-confirmation-dialog"; +import { SwapConfirmationDialog, useSwapProgress } from "../swap-confirmation-dialog"; import { useDerivedTwapSwapData, useInputLabels, @@ -17,7 +17,6 @@ import { import { OrderDetails } from "@/components/order-details"; import { useToExactAmount } from "../hooks"; import { useAccount } from "wagmi"; -import { useSwapState } from "../use-swap-state"; import { SwapStatus } from "@orbs-network/swap-ui"; import { Address, hexToNumber } from "viem"; import { @@ -37,7 +36,7 @@ import { simulateContract, writeContract, } from "wagmi/actions"; -import { SwapState } from "../use-swap-state"; +import { SwapState } from "../use-swap-progress"; import { useWaitForNewOrderCallback } from "./orders/use-orders-query"; @@ -55,7 +54,7 @@ export function TwapConfirmationDialog({ const dstAmount = useToExactAmount(destTokenAmount, outToken?.decimals); const outAmountUsd = useOutTokenUsd(); const inAmountUsd = useInTokenUsd(); - const { state, updateState, resetState } = useSwapState(); + const { state, updateState, resetState } = useSwapProgress(); const parsedSteps = useParsedSteps(state.steps); const { inputLabel, outputLabel } = useInputLabels(); const { requiresApproval, approvalLoading } = useGetRequiresApproval( diff --git a/src/trade/use-swap-state.ts b/src/trade/use-swap-state.ts deleted file mode 100644 index 2f37ba1..0000000 --- a/src/trade/use-swap-state.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { SwapStatus } from "@orbs-network/swap-ui"; -import { useCallback, useReducer } from "react"; - -export type SwapState = { - swapStatus?: SwapStatus; - currentStep?: number; - shouldUnwrap?: boolean; - txHash?: string; - steps?: number[]; - error?: string; - stapStatus?: SwapStatus; -}; - - -type Action = - | { type: "UPDATE_STATE"; payload: Partial } - | { type: "RESET" }; - -function reducer( - state: SwapState, - action: Action, - initialState: SwapState -): SwapState { - switch (action.type) { - case "UPDATE_STATE": - return { ...state, ...action.payload }; - case "RESET": - return initialState; - default: - return state; - } -} - -const initialState = {} as SwapState; - -export const useSwapState = () => { - const [state, dispatch] = useReducer( - (state: SwapState, action: Action) => reducer(state, action, initialState), - initialState - ); - - const updateState = useCallback( - (payload: Partial) => { - dispatch({ type: "UPDATE_STATE", payload }); - }, - [dispatch] - ); - - const resetState = useCallback(() => { - dispatch({ type: "RESET" }); - }, [dispatch]); - - return { - state, - updateState, - resetState, - }; -}; diff --git a/src/types.ts b/src/types.ts index 9346d5d..1a0f342 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,4 +20,3 @@ export enum SwapSteps { Swap, } -export type LiquidityProvider = 'paraswap' | 'liquidityhub' diff --git a/yarn.lock b/yarn.lock index f9bd531..883989b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -983,10 +983,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@orbs-network/liquidity-hub-sdk@^1.0.40": - version "1.0.40" - resolved "https://registry.yarnpkg.com/@orbs-network/liquidity-hub-sdk/-/liquidity-hub-sdk-1.0.40.tgz#8def63421714e7fd41ecd91ae7ed8ae1a52af3ab" - integrity sha512-/5uwdfSzPIb9uN4JxjEUfh+TgTIbZkOTGjR2UjsrZcjcXRBSg9mXB3GuAKD7g/ROIs9vrM1quuv/cdEQnVwrVw== +"@orbs-network/liquidity-hub-sdk@^1.0.41": + version "1.0.41" + resolved "https://registry.yarnpkg.com/@orbs-network/liquidity-hub-sdk/-/liquidity-hub-sdk-1.0.41.tgz#9d19adf60df4a546217eb2f08f9691856bd8416e" + integrity sha512-dYDtvCwYI6y0zXYQs8VgrlR+XlFyxgG4a+FOtOHcHCX8ZlPyp7DiwppFl9ly4qMEKM0IFxlnny1fyOTWAHYmuA== "@orbs-network/swap-ui@^0.0.14": version "0.0.14" From 5ceb52ebbc0e0423afc16f7c250cf02a5242db8a Mon Sep 17 00:00:00 2001 From: denis-orbs Date: Wed, 30 Oct 2024 17:17:51 +0200 Subject: [PATCH 2/3] 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} From 351418004194d86308d3234c6a01dcb6b0cb9170 Mon Sep 17 00:00:00 2001 From: denis-orbs Date: Mon, 4 Nov 2024 12:45:30 +0200 Subject: [PATCH 3/3] a --- index.html | 2 +- package.json | 2 +- src/App.tsx | 30 +- src/assets/orbslogo.svg | 237 +++++++++++ src/components/header.tsx | 5 +- src/components/tokens/token-card.tsx | 11 +- src/components/tokens/token-select.tsx | 120 ++++-- src/lib/index.ts | 4 +- src/lib/networks.ts | 383 ++++++++++++++++-- src/lib/useBalances.ts | 121 ------ src/lib/useDefaultTokens.ts | 60 +-- src/lib/useGetRequiresApproval.ts | 2 +- src/lib/useHandleInputError.ts | 21 +- src/lib/useParaswap.ts | 12 +- src/lib/usePriceUsd.ts | 14 +- src/lib/useTokenList.ts | 79 ---- src/lib/useTokens.ts | 205 ++++++++++ src/lib/useTokensWithBalances.ts | 32 -- src/lib/utils.ts | 3 +- src/lib/wagmi-config.ts | 8 +- src/store.ts | 18 + src/tokens.json | 0 src/trade/hooks.ts | 4 +- src/trade/liquidity-hub/context.tsx | 212 +++++----- src/trade/liquidity-hub/hooks.ts | 11 +- .../liquidity-hub-confirmation-dialog.tsx | 54 ++- .../liquidity-hub/liquidity-hub-swap.tsx | 149 +------ src/trade/liquidity-hub/swap-details.tsx | 64 +++ src/trade/settings.tsx | 43 ++ src/trade/trade.tsx | 28 -- src/trade/twap/components/inputs.tsx | 2 +- .../twap/components/limit-price-input.tsx | 4 +- src/trade/twap/components/price-toggle.tsx | 2 +- src/trade/twap/components/src-chunk-size.tsx | 2 +- .../twap/{twap-context.tsx => context.tsx} | 15 +- src/trade/twap/hooks.ts | 2 +- src/trade/twap/orders/orders.tsx | 10 +- src/trade/twap/orders/use-orders-query.ts | 2 +- src/trade/twap/twap-confirmation-dialog.tsx | 2 +- src/trade/twap/twap.tsx | 68 +--- tsconfig.app.tsbuildinfo | 2 +- yarn.lock | 8 +- 42 files changed, 1290 insertions(+), 763 deletions(-) create mode 100644 src/assets/orbslogo.svg delete mode 100644 src/lib/useBalances.ts delete mode 100644 src/lib/useTokenList.ts create mode 100644 src/lib/useTokens.ts delete mode 100644 src/lib/useTokensWithBalances.ts create mode 100644 src/store.ts create mode 100644 src/tokens.json create mode 100644 src/trade/liquidity-hub/swap-details.tsx create mode 100644 src/trade/settings.tsx delete mode 100644 src/trade/trade.tsx rename src/trade/twap/{twap-context.tsx => context.tsx} (89%) diff --git a/index.html b/index.html index 02cfa8c..070b142 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Orbs Demo + Orbs Playground
diff --git a/package.json b/package.json index 0f0f878..63f241e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@ethersproject/hash": "^5.7.0", - "@orbs-network/liquidity-hub-sdk": "^1.0.41", + "@orbs-network/liquidity-hub-sdk": "^1.0.44", "@orbs-network/swap-ui": "^0.0.14", "@orbs-network/twap-sdk": "^2.0.38", "@paraswap/sdk": "^6.10.0", diff --git a/src/App.tsx b/src/App.tsx index 309f0b8..ac22d34 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,35 @@ -import { Header } from '@/components/header' -import { Trade } from './trade/trade' +import { Header } from "@/components/header"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; +import { SwapLiquidityHub } from "./trade/liquidity-hub/liquidity-hub-swap"; +import { Settings } from "./trade/settings"; +import { SwapTwap, SwapLimit } from "./trade/twap/twap"; export function App() { return ( <>
- +
+

Trade

+ + + + Swap + TWAP + Limit + + + + + + + + + + + +
- ) + ); } diff --git a/src/assets/orbslogo.svg b/src/assets/orbslogo.svg new file mode 100644 index 0000000..45d64d9 --- /dev/null +++ b/src/assets/orbslogo.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/header.tsx b/src/components/header.tsx index d6b42c7..9684d96 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,12 +1,13 @@ import { ThemeToggle } from '@/components/theme-toggle' import { ConnectButton } from '@rainbow-me/rainbowkit' +import Logo from '@/assets/orbslogo.svg' export function Header() { return (
- - Orbs Demo + + Orbs Playground
diff --git a/src/components/tokens/token-card.tsx b/src/components/tokens/token-card.tsx index c2d4d5b..f1011e6 100644 --- a/src/components/tokens/token-card.tsx +++ b/src/components/tokens/token-card.tsx @@ -3,14 +3,7 @@ import { Card } from "../ui/card"; import { TokenSelect } from "./token-select"; import { Token } from "@/types"; import { NumericFormat } from "react-number-format"; -import { - format, - cn, - ErrorCodes, - useTokensWithBalances, - useTokenBalance, - toExactAmount, -} from "@/lib"; +import { format, cn, ErrorCodes, useTokenBalance, toExactAmount } from "@/lib"; import { Skeleton } from "../ui/skeleton"; import { Button } from "../ui/button"; import { useToExactAmount } from "@/trade/hooks"; @@ -50,7 +43,7 @@ export function TokenCard({ amountLoading, inputError, }: TokenCardProps) { - const balance = useTokenBalance(selectedToken?.address); + const { balance } = useTokenBalance(selectedToken?.address); const balanceError = inputError === ErrorCodes.InsufficientBalance; const balanceDisplay = selectedToken ? format.crypto(Number(toExactAmount(balance, selectedToken.decimals))) diff --git a/src/components/tokens/token-select.tsx b/src/components/tokens/token-select.tsx index fd919e2..110ed72 100644 --- a/src/components/tokens/token-select.tsx +++ b/src/components/tokens/token-select.tsx @@ -13,7 +13,16 @@ import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Token } from "@/types"; import { Card } from "../ui/card"; import { useMemo, useState } from "react"; -import { format, toExactAmount, useTokensWithBalances } from "@/lib"; +import { + eqIgnoreCase, + format, + usePriceUsd, + useSortedTokens, + useTokenBalance, +} from "@/lib"; +import { useToExactAmount } from "@/trade/hooks"; +import { Skeleton } from "../ui/skeleton"; +import { Virtuoso } from "react-virtuoso"; import BN from "bignumber.js"; type TokenSelectProps = { @@ -26,49 +35,19 @@ export function TokenSelect({ onSelectToken, }: TokenSelectProps) { const [open, setOpen] = useState(false); - const tokens = useTokensWithBalances().tokensWithBalances + const tokens = useSortedTokens(); const [filterInput, setFilterInput] = useState(""); - const SortedTokens = useMemo(() => { - if(!tokens) return null; - return Object.values(tokens) - .filter((t) => { - return ( - t.token.symbol.toLowerCase().includes(filterInput.toLowerCase()) || - t.token.address.toLowerCase().includes(filterInput.toLowerCase()) - ); - }) - .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) => ( - { - onSelectToken(t.token); - setOpen(false); - }} - > -
- - - - {t.token.symbol.charAt(0)} - - -
-
{t.token.symbol}
-
{t.token.name}
-
-
-
{format.crypto(Number(toExactAmount(t.balance.toString(), t.token.decimals) || '0'))}
-
- )); - }, [filterInput, onSelectToken, tokens]); + const filteredTokens = useMemo(() => { + if (!filterInput) return tokens || []; + return ( + tokens?.filter( + (t) => + eqIgnoreCase(t.address, filterInput) || + t.symbol.toLowerCase().includes(filterInput.toLowerCase()) + ) || [] + ); + }, [tokens, filterInput]); return ( setOpen(o)}> @@ -108,9 +87,64 @@ export function TokenSelect({ />
- {SortedTokens} + { + const token = filteredTokens[index]; + + const onSelect = () => { + onSelectToken(token); + setOpen(false); + }; + + return ; + }} + />
); } + +const TokenDisplay = ({ + token: t, + onSelect, +}: { + token: Token; + onSelect: () => void; +}) => { + const { balance, isLoading } = useTokenBalance(t.address); + const usd = usePriceUsd(t.address).data || 0; + const balanceUi = useToExactAmount(balance, t.decimals) || "0"; + const usdAmount = BN(balanceUi).multipliedBy(usd).toFixed() + + return ( + +
+ + + + {t.symbol.charAt(0)} + + +
+
{t.symbol}
+
{t.name}
+
+
+ {isLoading ? ( + + ) : ( +
+

{format.crypto(Number(balanceUi))}

+

${format.crypto(Number(usdAmount))}

+
+ )} +
+ ); +}; diff --git a/src/lib/index.ts b/src/lib/index.ts index 76d4dcb..1e4a570 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -3,11 +3,11 @@ export * from './networks' export * from './useGetRequiresApproval' export * from './utils' export * from './useHandleInputError' -export * from './useTokensWithBalances' +export * from './useTokens' export * from './useDefaultTokens' export * from './useDebounce' export * from './useParaswap' export * from './wagmi-config' -export * from './useTokenList' +export * from './useTokens' export * from './useWrapOrUnwrapOnly' export * from './usePriceUsd' diff --git a/src/lib/networks.ts b/src/lib/networks.ts index 472acc1..7eb1307 100644 --- a/src/lib/networks.ts +++ b/src/lib/networks.ts @@ -1,54 +1,383 @@ -import { zeroAddress } from 'viem' +import { zeroAddress } from "viem"; export const networks = { + eth: { + id: 1, + name: "Ethereum", + shortname: "eth", + native: { + address: zeroAddress, + symbol: "ETH", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/ethereum.svg", + }, + wToken: { + symbol: "WETH", + address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", + }, + publicRpcUrl: "https://eth.llamarpc.com", + logoUrl: "https://app.1inch.io/assets/images/network-logos/ethereum.svg", + explorer: "https://etherscan.io", + eip1559: true, + baseAssets: [ + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", + "0x853d955aCEf822Db058eb8505911ED77F175b99e", + "0xff56Cc6b1E6dEd347aA0B7676C85AB0B3D08B0FA", + ], + }, + bsc: { + id: 56, + name: "BinanceSmartChain", + shortname: "bsc", + native: { + address: zeroAddress, + symbol: "BNB", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/bsc_2.svg", + }, + wToken: { + symbol: "WBNB", + address: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c.png", + }, + publicRpcUrl: "https://bsc-dataseed.binance.org", + logoUrl: "https://app.1inch.io/assets/images/network-logos/bsc_2.svg", + explorer: "https://bscscan.com", + eip1559: false, + baseAssets: [ + "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c", + "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "0x55d398326f99059fF775485246999027B3197955", + "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3", + "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", + "0x2170Ed0880ac9A755fd29B2688956BD959F933F8", + "0xeBd49b26169e1b52c04cFd19FCf289405dF55F80", + ], + }, poly: { id: 137, - name: 'Polygon', - shortname: 'poly', + name: "Polygon", + shortname: "poly", native: { address: zeroAddress, - symbol: 'MATIC', + symbol: "MATIC", decimals: 18, - logoUrl: 'https://app.1inch.io/assets/images/network-logos/polygon.svg', + logoUrl: "https://app.1inch.io/assets/images/network-logos/polygon.svg", }, wToken: { - symbol: 'WMATIC', - address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', + symbol: "WMATIC", + address: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", decimals: 18, weth: true, logoUrl: - 'https://tokens-data.1inch.io/images/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270.png', + "https://tokens-data.1inch.io/images/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270.png", }, - publicRpcUrl: 'https://polygon-rpc.com', - logoUrl: 'https://app.1inch.io/assets/images/network-logos/polygon.svg', - explorer: 'https://polygonscan.com', + publicRpcUrl: "https://polygon-rpc.com", + logoUrl: "https://app.1inch.io/assets/images/network-logos/polygon.svg", + explorer: "https://polygonscan.com", eip1559: true, + baseAssets: [ + "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", + "0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4", + "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", + "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", + "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", + "0xdAb529f40E671A1D4bF91361c21bf9f0C9712ab7", + "0x614389EaAE0A6821DC49062D56BDA3d9d45Fa2ff", + ], + }, + arb: { + id: 42161, + name: "Arbitrum", + shortname: "arb", + native: { + address: zeroAddress, + symbol: "ETH", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/ethereum.svg", + }, + wToken: { + symbol: "WETH", + address: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", + }, + publicRpcUrl: "https://arb1.arbitrum.io/rpc", + logoUrl: "https://app.1inch.io/assets/images/network-logos/arbitrum.svg", + explorer: "https://arbiscan.io", + eip1559: true, + baseAssets: [ + "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", + "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", + "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + "0x912CE59144191C1204E64559FE8253a0e49E6548", + "0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F", + "0x4D15a3A2286D883AF0AA1B3f21367843FAc63E07", + ], + }, + avax: { + id: 43114, + name: "Avalanche", + shortname: "avax", + native: { + address: zeroAddress, + symbol: "AVAX", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/avalanche.svg", + }, + wToken: { + symbol: "WAVAX", + address: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7.png", + }, + publicRpcUrl: "https://api.avax.network/ext/bc/C/rpc", + logoUrl: "https://app.1inch.io/assets/images/network-logos/avalanche.svg", + explorer: "https://snowtrace.io", + eip1559: true, + baseAssets: [ + "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + "0x50b7545627a5162F82A992c33b87aDc75187B218", + "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664", + "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", + "0xc7198437980c041c805A1EDcbA50c1Ce5db95118", + "0x19860CCB0A68fd4213aB9D8266F7bBf05A8dDe98", + "0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB", + "0xd586E7F844cEa2F87f50152665BCbc2C279D8d70", + "0x340fE1D898ECCAad394e2ba0fC1F93d27c7b717A", + ], + }, + oeth: { + id: 10, + name: "Optimism", + shortname: "oeth", + native: { + address: zeroAddress, + symbol: "ETH", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/ethereum.svg", + }, + wToken: { + symbol: "WETH", + address: "0x4200000000000000000000000000000000000006", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", + }, + publicRpcUrl: "https://mainnet.optimism.io", + logoUrl: "https://app.1inch.io/assets/images/network-logos/optimism.svg", + explorer: "https://optimistic.etherscan.io", + eip1559: true, + baseAssets: [ + "0x4200000000000000000000000000000000000006", + "0x68f180fcCe6836688e9084f035309E29Bf0A2095", + "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + "0x4200000000000000000000000000000000000042", + "0x2E3D870790dC77A83DD1d18184Acc7439A53f475", + ], + }, + ftm: { + id: 250, + name: "Fantom", + shortname: "ftm", + native: { + address: zeroAddress, + symbol: "FTM", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/fantom.svg", + }, + wToken: { + symbol: "WFTM", + address: "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0x4e15361fd6b4bb609fa63c81a2be19d873717870.png", + }, + publicRpcUrl: "https://rpc.ftm.tools", + logoUrl: "https://app.1inch.io/assets/images/network-logos/fantom.svg", + explorer: "https://ftmscan.com", + eip1559: true, + baseAssets: [ + "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83", + "0x321162Cd933E2Be498Cd2267a90534A804051b11", + "0x74b23882a30290451A17c44f4F05243b6b58C76d", + "0x04068DA6C83AFCFA0e13ba15A6696662335D5B75", + "0x8D11eC38a3EB5E956B052f67Da8Bdc9bef8Abf3E", + "0xdc301622e621166BD8E82f2cA0A26c13Ad0BE355", + "0x3E01B7E242D5AF8064cB9A8F9468aC0f8683617c", + ], + }, + base: { + id: 8453, + name: "Base", + shortname: "base", + native: { + address: zeroAddress, + symbol: "ETH", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/ethereum.svg", + }, + wToken: { + symbol: "WETH", + address: "0x4200000000000000000000000000000000000006", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", + }, + publicRpcUrl: "https://mainnet.base.org", + logoUrl: "https://app.1inch.io/assets/images/network-logos/base.svg", + explorer: "https://basescan.org", + eip1559: false, + baseAssets: [ + "0x4200000000000000000000000000000000000006", + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + ], + }, + linea: { + id: 59144, + name: "Linea", + shortname: "linea", + native: { + address: zeroAddress, + symbol: "ETH", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/ethereum.svg", + }, + wToken: { + symbol: "WETH", + address: "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", + }, + publicRpcUrl: "https://rpc.linea.build", + logoUrl: "https://lineascan.build/images/logo.svg", + explorer: "https://lineascan.build", + eip1559: false, + baseAssets: [ + "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f", + "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", + "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", + ], + }, + zksync: { + id: 324, + name: "zksync", + shortname: "zksync", + native: { + address: zeroAddress, + symbol: "ETH", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/ethereum.svg", + }, + wToken: { + symbol: "WETH", + address: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", + }, + publicRpcUrl: "https://mainnet.era.zksync.io", + logoUrl: + "https://raw.githubusercontent.com/matter-labs/zksync/0a4ca2145a0c95b5bafa84c2f095c644907a8825/zkSyncLogo.svg", + explorer: "https://explorer.zksync.io/", + eip1559: true, + baseAssets: [ + "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91", + "0x3355df6d4c9c3035724fd0e3914de96a5a83aaf4", + "0x4b9eb6c0b6ea15176bbf62841c6b2a8a398cb656", + ], + }, + zkevm: { + id: 1101, + name: "zkevm", + shortname: "zkevm", + native: { + address: zeroAddress, + symbol: "ETH", + decimals: 18, + logoUrl: "https://app.1inch.io/assets/images/network-logos/ethereum.svg", + }, + wToken: { + symbol: "WETH", + address: "0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9", + decimals: 18, + weth: true, + logoUrl: + "https://tokens-data.1inch.io/images/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", + }, + publicRpcUrl: "https://zkevm-rpc.com", + logoUrl: + "https://user-images.githubusercontent.com/18598517/235932702-bc47eae5-d672-4dd9-9da2-8ea8f51a93f3.png", + explorer: "https://zkevm.polygonscan.com/", + eip1559: true, + baseAssets: [ + "0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9", + "0xA8CE8aee21bC2A48a5EF670afCc9274C7bbbC035", + "0xC5015b9d9161Dca7e18e32f6f25C4aD850731Fd4", + ], }, -} - -export const network = { - 137: { - id: 137, - name: 'Polygon', - shortname: 'poly', + blast: { + id: 81457, + name: "blast", + shortname: "blast", native: { address: zeroAddress, - symbol: 'MATIC', + symbol: "ETH", decimals: 18, - logoUrl: 'https://app.1inch.io/assets/images/network-logos/polygon.svg', + logoUrl: "https://icons.llamao.fi/icons/chains/rsz_blast", }, wToken: { - symbol: 'WMATIC', - address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', + symbol: "WETH", + address: "0x4300000000000000000000000000000000000004", decimals: 18, weth: true, logoUrl: - 'https://tokens-data.1inch.io/images/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270.png', + "https://tokens-data.1inch.io/images/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png", }, - publicRpcUrl: 'https://polygon-rpc.com', - logoUrl: 'https://app.1inch.io/assets/images/network-logos/polygon.svg', - explorer: 'https://polygonscan.com', + publicRpcUrl: "https://rpc.ankr.com/blast", + logoUrl: "https://icons.llamao.fi/icons/chains/rsz_blast", + explorer: "https://blastscan.io/", eip1559: true, + baseAssets: [ + "0x4300000000000000000000000000000000000004", + "0x4300000000000000000000000000000000000003", + ], }, -} \ No newline at end of file +}; + +export const getNetwork = (chainId: number) => { + return Object.values(networks).find((network) => network.id === chainId); +}; + diff --git a/src/lib/useBalances.ts b/src/lib/useBalances.ts deleted file mode 100644 index 15f870f..0000000 --- a/src/lib/useBalances.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { getBalance, multicall } from '@wagmi/core' -import { Config, serialize, useBalance, useConfig } from 'wagmi' -import { GetBalanceReturnType } from 'wagmi/actions' -import { Token, TokensWithBalances } from '@/types' -import { erc20Abi, isAddress, Address } from 'viem' -import { zeroAddress } from '@orbs-network/liquidity-hub-sdk' - -interface QueryBalanceParams { - chainId: number | undefined - tokens: Token[] - account: string | undefined - nativeBalance?: GetBalanceReturnType - config: Config -} - -export const queryFnUseBalances = async ({ - chainId, - tokens, - account, - nativeBalance, - config, -}: QueryBalanceParams) => { - if (!account || !chainId || !tokens) return null - - let native = nativeBalance - if (typeof native === 'undefined') { - native = await getBalance(config, { - address: account as Address, - chainId, - }) - } - - const [validatedTokens, validatedTokenAddresses] = tokens.reduce< - [Token[], Address[]] - >( - (acc, tokens) => { - if (chainId && tokens && isAddress(tokens.address)) { - acc[0].push(tokens) - acc[1].push(tokens.address as Address) - } - - return acc - }, - [[], []] - ) - - const data = await multicall(config, { - contracts: validatedTokenAddresses.map( - (token) => - ({ - chainId, - address: token, - abi: erc20Abi, - functionName: 'balanceOf', - args: [account], - } as const) - ), - }) - - const _data = data.reduce((acc, _cur, i) => { - const amount = data[i].result - if (typeof amount === 'bigint') { - acc[validatedTokens[i].address] = { - token: validatedTokens[i], - balance: amount, - } - } - return acc - }, {}) - - _data[zeroAddress] = { - token: validatedTokens[0], - balance: native.value, - } - - return _data -} - -interface UseBalanceParams { - chainId: number | undefined - tokens: Token[] - account: Address | undefined - enabled?: boolean -} - -export const useBalances = ({ - chainId, - tokens, - account, - enabled = true, -}: UseBalanceParams) => { - const { data: nativeBalance, queryKey } = useBalance({ - chainId, - address: account, - query: { enabled, refetchInterval: 10000, staleTime: 10000 }, - }) - - const config = useConfig() - - return { - query: useQuery({ - queryKey: [ - 'useBalances', - { chainId, tokens, account, nativeBalance: serialize(nativeBalance) }, - ], - queryFn: () => - queryFnUseBalances({ - chainId, - tokens, - account, - nativeBalance, - config, - }), - refetchInterval: 10000, - staleTime: 10000, - enabled: Boolean(chainId && account && enabled && tokens), - }), - queryKey: [...queryKey, 'useBalances'], - } -} diff --git a/src/lib/useDefaultTokens.ts b/src/lib/useDefaultTokens.ts index 96b5024..fa44c93 100644 --- a/src/lib/useDefaultTokens.ts +++ b/src/lib/useDefaultTokens.ts @@ -1,48 +1,14 @@ -import { Token, TokensWithBalances } from '@/types' -import { zeroAddress } from '@orbs-network/liquidity-hub-sdk' -import { useEffect, useMemo } from 'react' - -/* Sets default tokens */ -type UseDefaultTokens = { - inToken: Token | null - outToken: Token | null - tokensWithBalances: TokensWithBalances | null | undefined - setInToken: (token: Token) => void - setOutToken: (token: Token) => void -} -export function useDefaultTokens({ - tokensWithBalances, - inToken, - outToken, - setInToken, - setOutToken, -}: UseDefaultTokens) { - const defaultTokens = useMemo(() => { - if (!tokensWithBalances) return [] - - return [ - tokensWithBalances[zeroAddress].token, - Object.values(tokensWithBalances).find((t) => t.token.symbol === 'USDT') - ?.token || null, - ].filter(Boolean) as Token[] - }, [tokensWithBalances]) - - useEffect(() => { - if (!inToken && tokensWithBalances) { - setInToken(defaultTokens[0]) - } - - if (!outToken && tokensWithBalances) { - setOutToken(defaultTokens[1]) - } - }, [ - inToken, - defaultTokens, - outToken, - setInToken, - setOutToken, - tokensWithBalances, - ]) - - return defaultTokens +import { useMemo } from "react"; +import { useSortedTokens } from "./useTokens"; + +export function useDefaultTokens() { + const tokens = useSortedTokens(); + + return useMemo(() => { + if (!tokens) return; + return { + inToken: tokens[0], + outToken: tokens[1], + }; + }, [tokens]); } diff --git a/src/lib/useGetRequiresApproval.ts b/src/lib/useGetRequiresApproval.ts index 8d0988b..788ced4 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?: any, inTokenAddress = '', inAmount = '' ) { diff --git a/src/lib/useHandleInputError.ts b/src/lib/useHandleInputError.ts index 2ee712a..750341d 100644 --- a/src/lib/useHandleInputError.ts +++ b/src/lib/useHandleInputError.ts @@ -1,10 +1,10 @@ -import { ErrorCodes, toExactAmount } from "@/lib/utils"; +import { ErrorCodes } from "@/lib/utils"; import { Token } from "@/types"; import { useMemo } from "react"; -import { - useTokenBalance, -} from "./useTokensWithBalances"; import BN from "bignumber.js"; +import { useTokenBalance } from "./useTokens"; +import { useToRawAmount } from "@/trade/hooks"; + /* Handles amount input errors */ @@ -15,15 +15,18 @@ export function useInputError({ inToken: Token | null; inputAmount: string; }) { - const tokenBalance = useTokenBalance(inToken?.address); + const {balance} = useTokenBalance(inToken?.address); + const parsedInputAmount = useToRawAmount(inputAmount, inToken?.decimals) return useMemo(() => { - if (!inputAmount) { + if(BN(inputAmount || '0').lte(0)) { + return ErrorCodes.EnterAmount; + } + if (!balance) { return ErrorCodes.EnterAmount; } - const balance = toExactAmount(tokenBalance, inToken?.decimals); - if (BN(inputAmount).gt(balance)) { + if (BN(parsedInputAmount).gt(balance)) { return ErrorCodes.InsufficientBalance; } - }, [inputAmount, inToken, tokenBalance]); + }, [inputAmount, inToken, balance, parsedInputAmount]); } diff --git a/src/lib/useParaswap.ts b/src/lib/useParaswap.ts index 47eb488..cd716ea 100644 --- a/src/lib/useParaswap.ts +++ b/src/lib/useParaswap.ts @@ -2,19 +2,11 @@ import { useCallback, useMemo } from 'react' import { getMinAmountOut, isNativeAddress, - resolveNativeTokenAddress, - waitForConfirmations, } from '@/lib/utils' import { constructSimpleSDK, OptimalRate, SwapSide } from '@paraswap/sdk' -import { useMutation, useQuery } from '@tanstack/react-query' +import { useQuery } from '@tanstack/react-query' import { useAccount } from 'wagmi' -import { wagmiConfig } from './wagmi-config' -import { estimateGas, sendTransaction } from 'wagmi/actions' -import { Address } from 'viem' -import { SwapStatus } from '@orbs-network/swap-ui' -import { SwapSteps } from '@/types' -import { approveAllowance } from './approveAllowance' -import { getRequiresApproval } from './getRequiresApproval' + const PARASWAP_NATIVE_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' diff --git a/src/lib/usePriceUsd.ts b/src/lib/usePriceUsd.ts index e81a13d..c273a39 100644 --- a/src/lib/usePriceUsd.ts +++ b/src/lib/usePriceUsd.ts @@ -1,4 +1,4 @@ -import { networks, isNativeAddress } from '@/lib' +import { isNativeAddress, getNetwork } from '@/lib' import { useQuery } from '@tanstack/react-query' import { useAccount } from 'wagmi' @@ -6,14 +6,14 @@ export const usePriceUsd = (address?: string) => { const {chainId} = useAccount() return useQuery({ queryKey: ['usePriceUSD', chainId, address], - queryFn: async () => { + queryFn: async () => { if (!address || !chainId) { return 0 } return (await fetchLLMAPrice(address, chainId)).priceUsd }, - refetchInterval: 10_000, + refetchInterval: 30_000, enabled: !!address && !!chainId, }) } @@ -40,7 +40,7 @@ export async function fetchLLMAPrice(token: string, chainId: number) { const chainName = chainIdToName[chainId] || 'Unknown Chain' if (isNativeAddress(token)) { - token = networks.poly.wToken.address + token = getNetwork(chainId)?.wToken.address || '' } const tokenAddressWithChainId = `${chainName}:${token}` const url = `https://coins.llama.fi/prices/current/${tokenAddressWithChainId}` @@ -49,10 +49,10 @@ export async function fetchLLMAPrice(token: string, chainId: number) { return nullPrice } const data = await response.json() - const coin = data.coins[tokenAddressWithChainId] + const coin = data.coins[tokenAddressWithChainId] return { - priceUsd: coin.price, - priceNative: coin.price, + priceUsd: !coin ? 0 : coin.price, + priceNative: !coin ? 0 : coin.price, timestamp: Date.now(), } } catch (error) { diff --git a/src/lib/useTokenList.ts b/src/lib/useTokenList.ts deleted file mode 100644 index f887dcd..0000000 --- a/src/lib/useTokenList.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { networks } from '@/lib/networks' -import { Token } from '@/types' -import { zeroAddress } from '@orbs-network/liquidity-hub-sdk' -import { useQuery } from '@tanstack/react-query' - -type PolygonToken = { - address: string - chainId: number - decimals: number - logoURI: string - name?: string - symbol: string -} - -const getPolygonTokens = async (): Promise => { - const res = await fetch( - 'https://unpkg.com/quickswap-default-token-list@1.3.16/build/quickswap-default.tokenlist.json' - ) - - if (!res.ok) { - throw new Error('Failed to fetch tokens') - } - - const polyTokens = (await res.json()).tokens as PolygonToken[] - - const tokens = polyTokens.filter((it) => it.chainId === networks.poly.id) - - const candiesAddresses = [ - zeroAddress, - '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', - '0x3A58a54C066FdC0f2D55FC9C89F0415C92eBf3C4', - '0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6', - '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', - '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', - '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063', - '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619', - '0xdAb529f40E671A1D4bF91361c21bf9f0C9712ab7', - '0x614389EaAE0A6821DC49062D56BDA3d9d45Fa2ff', - ] - - const sorted = tokens.sort((a, b) => { - const indexA = candiesAddresses.indexOf(a.address) - const indexB = candiesAddresses.indexOf(b.address) - return indexB - indexA - }) - - return [ - { - address: zeroAddress, - symbol: 'MATIC', - decimals: 18, - logoURI: 'https://app.1inch.io/assets/images/network-logos/polygon.svg', - name: 'MATIC', - }, - ...sorted, - ].map((token) => { - return { - address: token.address, - symbol: token.symbol, - decimals: token.decimals, - logoUrl: token.logoURI?.replace('/logo_24.png', '/logo_48.png'), - name: token.name, - } - }) -} - - - -export function useTokensList() { - const chainId = networks.poly.id - - return useQuery({ - queryFn: async () => { - return getPolygonTokens() || [] - }, - queryKey: ['tokens-list', chainId], - staleTime: Infinity, - }) -} diff --git a/src/lib/useTokens.ts b/src/lib/useTokens.ts new file mode 100644 index 0000000..ebf63e7 --- /dev/null +++ b/src/lib/useTokens.ts @@ -0,0 +1,205 @@ +import { Token } from "@/types"; +import { getNetwork, networks } from "./networks"; +import { useQuery } from "@tanstack/react-query"; +import { getBalance, multicall } from "@wagmi/core"; +import { useAccount, useConfig } from "wagmi"; +import { erc20Abi, Address } from "viem"; +import { zeroAddress } from "@orbs-network/liquidity-hub-sdk"; +import { eqIgnoreCase } from "./utils"; +import { useMemo } from "react"; + +const getFantomTokens = async (signal?: AbortSignal): Promise => { + const res = await fetch( + "https://raw.githubusercontent.com/viaprotocol/tokenlists/main/tokenlists/ftm.json", + { signal } + ); + const data = await res.json(); + return data.map((token: any) => { + return { + address: token.address, + symbol: token.symbol, + decimals: token.decimals, + logoUrl: token.logoURI, + name: token.name, + }; + }); +}; + +const getSushiTokens = async ( + chainId: number, + signal?: AbortSignal +): Promise => { + const tokens = await fetch("https://token-list.sushi.com/", { signal }).then( + (res) => + res + .json() + .then((it) => it.tokens.filter((it: any) => it.chainId === chainId)) + ); + + return Object.values(tokens).map((token: any) => { + return { + address: token.address, + symbol: token.symbol, + decimals: token.decimals, + logoUrl: token.logoURI, + name: token.name, + }; + }); +}; + +const getLineaTokens = async (signal?: AbortSignal): Promise => { + const tokens = await fetch("https://api.lynex.fi/api/v1/assets", { signal }) + .then((res) => res.json()) + .then((res) => res.data); + return tokens.map((token: any) => { + return { + address: token.address, + symbol: token.symbol, + decimals: token.decimals, + logoUrl: token.logoURI, + name: token.name, + }; + }); +}; + +const fetchTokens = async ( + chainId: number, + signal?: AbortSignal +): Promise => { + let tokens: Token[] = []; + if (chainId === networks.linea.id) { + tokens = await getLineaTokens(signal); + } else if (chainId === networks.ftm.id) { + tokens = await getFantomTokens(signal); + } else { + tokens = await getSushiTokens(chainId, signal); + } + const network = getNetwork(chainId); + if (network) { + const nativeToken: Token = { + address: network.native.address, + symbol: network.native.symbol, + decimals: network.native.decimals, + logoUrl: network.native.logoUrl, + }; + + tokens = [nativeToken, ...tokens]; + } + + const baseAssets = getNetwork(chainId)?.baseAssets; + if (!baseAssets) { + return tokens; + } + const sortedTokens = tokens.sort((a, b) => { + const aPriority = baseAssets.includes(a.address) ? 0 : 1; + const bPriority = baseAssets.includes(b.address) ? 0 : 1; + if (aPriority !== bPriority) { + return aPriority - bPriority; + } + return a.address.localeCompare(b.address); + }); + + return sortedTokens; +}; + +const useTokensList = () => { + const chainId = useAccount().chainId; + + return useQuery({ + queryFn: async ({ signal }) => { + const response = await fetchTokens(chainId!, signal); + return response; + }, + queryKey: ["useTokensList", chainId], + staleTime: Infinity, + enabled: !!chainId, + }); +}; + +type BalancesReponse = Record; + +export const useTokenBalaces = () => { + const { data: tokens } = useTokensList(); + const { address: account, chainId } = useAccount(); + + const config = useConfig(); + + return useQuery({ + queryKey: ["useBalances", chainId, account, tokens?.map((t) => t.address)], + queryFn: async () => { + if (!tokens) return {}; + let native = await getBalance(config, { + address: account as Address, + chainId, + }); + + const addresses = tokens + .map((token) => token.address) + .filter((it) => !eqIgnoreCase(it, zeroAddress)); + + const multicallResponse = await (multicall as any)(config, { + contracts: addresses.map( + (address) => + ({ + chainId, + address, + abi: erc20Abi, + functionName: "balanceOf", + args: [account], + } as const) + ), + }); + + + const balances = addresses.reduce( + (acc: any, address: any, index: number) => { + acc[address] = multicallResponse[index].result?.toString() || '0'; + return acc; + }, + {} + ); + + balances[zeroAddress] = native.value.toString(); + + return balances; + }, + refetchInterval: 20_000, + staleTime: Infinity, + enabled: Boolean(chainId && account && tokens?.length), + }); +}; + +export const useTokenBalance = (tokenAddress?: string) => { + const { data: balances, isLoading } = useTokenBalaces(); + return useMemo(() => { + if (!tokenAddress) { + return { + isLoading, + balance: "0", + }; + } + return { + isLoading, + balance: balances?.[tokenAddress] || "0", + }; + }, [balances, tokenAddress, isLoading]); +}; + +export const useSortedTokens = () => { + const { data: tokens } = useTokensList(); + const { data: balances } = useTokenBalaces(); + return useMemo(() => { + const sorted = tokens?.sort((a, b) => { + const balanceA = BigInt(balances?.[a.address] || "0"); + const balanceB = BigInt(balances?.[b.address] || "0"); + return balanceB > balanceA ? 1 : balanceB < balanceA ? -1 : 0; + }); + + const native = sorted?.find((it) => eqIgnoreCase(it.address, zeroAddress)); + if (native) { + sorted?.splice(sorted.indexOf(native), 1); + sorted?.unshift(native); + } + return sorted; + }, [tokens, balances]); +}; diff --git a/src/lib/useTokensWithBalances.ts b/src/lib/useTokensWithBalances.ts deleted file mode 100644 index c1d2bdc..0000000 --- a/src/lib/useTokensWithBalances.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useAccount } from "wagmi"; -import { useTokensList } from "./useTokenList"; -import { useBalances } from "./useBalances"; -import { TokensWithBalances } from "@/types"; - -export function useTokensWithBalances() { - const { address, chainId } = useAccount(); - const { data: tokens, isLoading: tokensLoading } = useTokensList(); - const { - query: { data: balances, isLoading: balancesLoading, refetch }, - queryKey, - } = useBalances({ - chainId, - tokens: tokens || [], - account: address, - enabled: Boolean(tokens && address && chainId), - }); - - return { - isLoading: tokensLoading || balancesLoading, - tokensWithBalances: balances as TokensWithBalances, - queryKey, - refetch, - }; -} - -export const useTokenBalance = (tokenAddress?: string) => { - const { tokensWithBalances } = useTokensWithBalances(); - return !tokenAddress - ? "" - : tokensWithBalances?.[tokenAddress]?.balance.toString(); -}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 58f6527..d832aae 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -21,7 +21,7 @@ export const toExactAmount = ( if (decimalScale) { return result.toFixed(decimalScale); } - return result.toString(); + return result.toFixed(); }; export const toRawAmount = (amount?: string, decimals?: number) => { if (!decimals || !amount) return ""; @@ -72,7 +72,6 @@ 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 diff --git a/src/lib/wagmi-config.ts b/src/lib/wagmi-config.ts index 68cb342..2db44ed 100644 --- a/src/lib/wagmi-config.ts +++ b/src/lib/wagmi-config.ts @@ -1,10 +1,14 @@ import { getDefaultConfig } from '@rainbow-me/rainbowkit' -import { polygon } from 'viem/chains' +import { http } from 'viem' +import { polygon, mainnet, arbitrum, bsc, fantom, blast, linea } from 'viem/chains' const walletConnectProjectId = import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID export const wagmiConfig = getDefaultConfig({ appName: 'DEX Playground', projectId: walletConnectProjectId, - chains: [polygon], + chains: [polygon, mainnet, arbitrum, bsc, fantom, blast, linea], + transports: { + [mainnet.id]: http(`https://rpcman.orbs.network/rpc?chainId=1&appId=dex-playground`) + } }) diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..a663c4b --- /dev/null +++ b/src/store.ts @@ -0,0 +1,18 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface AppStore { + slippage: number; + setSlippage: (slippage: number) => void; +} +export const useAppState = create( + persist( + (set) => ({ + slippage: 0.5, + setSlippage: (slippage: number) => set({ slippage }), + }), + { + name: "main-store", + } + ) +); diff --git a/src/tokens.json b/src/tokens.json new file mode 100644 index 0000000..e69de29 diff --git a/src/trade/hooks.ts b/src/trade/hooks.ts index 9681e40..9cb69f1 100644 --- a/src/trade/hooks.ts +++ b/src/trade/hooks.ts @@ -1,4 +1,4 @@ -import { network, toExactAmount, toRawAmount } from "@/lib"; +import { getNetwork, toExactAmount, toRawAmount } from "@/lib"; import { useMemo } from "react"; import { useChainId } from "wagmi"; @@ -15,7 +15,7 @@ export const useNetwork = () => { return useMemo(() => { if (!chainId) return; - return network[chainId as keyof typeof network]; + return getNetwork(chainId); }, [chainId]); }; diff --git a/src/trade/liquidity-hub/context.tsx b/src/trade/liquidity-hub/context.tsx index 948b1ab..0780cee 100644 --- a/src/trade/liquidity-hub/context.tsx +++ b/src/trade/liquidity-hub/context.tsx @@ -1,107 +1,119 @@ -import { useDefaultTokens, useTokensWithBalances } from "@/lib"; +import { useDefaultTokens } 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 { + 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; + inToken: null, + outToken: null, + inputAmount: "", + acceptedQuote: undefined, + liquidityHubDisabled: false, + forceLiquidityHub: false, + showConfirmation: false, +}; + +interface State { + inToken: Token | null; + outToken: Token | null; + inputAmount: string; + acceptedQuote: Quote | undefined; + liquidityHubDisabled: boolean; + 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; } - - const Context = createContext({} as ContextType); - export const useLiquidityHubSwapContext = () => { - return useContext(Context); - }; - +}; + +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 defaultTokens = useDefaultTokens(); + + const state = useMemo(() => { + return { + ..._state, + inToken: _state.inToken || defaultTokens?.inToken || null, + outToken: _state.outToken || defaultTokens?.outToken || null, + }; + }, [_state, defaultTokens]); + + const { chainId } = useAccount(); + + const parsedInputAmount = useToRawAmount( + state.inputAmount, + state.inToken?.decimals + ); + + const updateState = useCallback( + (payload: Partial) => { + dispatch({ type: "UPDATE", payload }); + }, + [dispatch] + ); + + const resetState = useCallback(() => { + dispatch({ type: "RESET" }); + }, [dispatch]); + + const sdk = useMemo( + () => constructSDK({ partner: "widget", chainId }), + [chainId] + ); - 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 + return ( + + {children} + + ); +}; diff --git a/src/trade/liquidity-hub/hooks.ts b/src/trade/liquidity-hub/hooks.ts index 54a8e34..5f3ede3 100644 --- a/src/trade/liquidity-hub/hooks.ts +++ b/src/trade/liquidity-hub/hooks.ts @@ -5,10 +5,10 @@ import { useInputError, getMinAmountOut, useGetRequiresApproval, - network, networks, isNativeAddress, } from "@/lib"; +import { useAppState } from "@/store"; import { permit2Address } from "@orbs-network/liquidity-hub-sdk"; import { useQueryClient, useQuery } from "@tanstack/react-query"; import { useMemo, useCallback } from "react"; @@ -19,9 +19,7 @@ import { useLiquidityHubSwapContext } from "./context"; export const QUOTE_REFETCH_INTERVAL = 20_000; export const useParaswapMinAmountOut = () => { - const { - state: { slippage }, - } = useLiquidityHubSwapContext(); + const { slippage } = useAppState(); const optimalRate = useOptimalRate().data; return useMemo(() => { return getMinAmountOut(slippage, optimalRate?.destAmount || "0"); @@ -31,8 +29,10 @@ export const useParaswapMinAmountOut = () => { export function useLiquidityHubQuote() { const queryClient = useQueryClient(); const { chainId, address: account } = useAccount(); + const { slippage } = useAppState(); + const { - state: { inToken, outToken, liquidityHubDisabled, slippage }, + state: { inToken, outToken, liquidityHubDisabled }, sdk, parsedInputAmount, } = useLiquidityHubSwapContext(); @@ -144,6 +144,7 @@ export const useLiquidityHubInputError = () => { const { state: { inToken, inputAmount }, } = useLiquidityHubSwapContext(); + return useInputError({ inputAmount, inToken, diff --git a/src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx b/src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx index 80d8237..f5e1a78 100644 --- a/src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx +++ b/src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx @@ -10,6 +10,7 @@ import { getErrorMessage, getLiquidityProviderName, getSteps, + isNativeAddress, promiseWithTimeout, toExactAmount, useParaswapBuildTxCallback, @@ -39,7 +40,8 @@ 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"; +import { useNetwork, useToExactAmount } from "../hooks"; +import { useAppState } from "@/store"; // Construct steps for swap to display in UI const useSteps = (steps?: number[]) => { @@ -107,9 +109,9 @@ export function LiquidityHubConfirmationDialog({ const approvalLoading = isLiquidityHubTrade ? liquidityHubApproval.approvalLoading : paraswapApproval.approvalLoading; + const onSubmit = useCallback(async () => { if (!isLiquidityHubTrade) { - console.log("Proceeding with Liquidity Hub"); swapWithParaswap(); } else { swapWithLiquidityHub(); @@ -241,23 +243,24 @@ const Details = () => { const { state: { outToken, isLiquidityHubTrade }, } = useLiquidityHubSwapContext(); - const outAmountUsd = optimalRate?.destUSD; - const outAmount = isLiquidityHubTrade - ? quote?.outAmount - : optimalRate?.destAmount; + const outTokenUsd = usePriceUsd(outToken?.address).data; const gasPrice = useMemo(() => { - 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]); + if (!isLiquidityHubTrade) { + return Number(optimalRate?.gasCostUSD || "0"); + } + + if (!outToken || !outTokenUsd) return 0; + const gas = toExactAmount(quote?.gasAmountOut, outToken.decimals); + + return Number(gas) * outTokenUsd; + }, [ + isLiquidityHubTrade, + optimalRate?.gasCostUSD, + outToken, + outTokenUsd, + quote?.gasAmountOut, + ]); return (
@@ -293,10 +296,12 @@ export const useParaswapSwapCallback = ( const buildParaswapTxCallback = useParaswapBuildTxCallback(); const optimalRate = useOptimalRate().data; const { - state: { slippage, inToken }, + state: { inToken }, } = useLiquidityHubSwapContext(); + const { slippage } = useAppState(); + const wToken = useNetwork()?.wToken.address; const requiresApproval = useParaswapApproval().requiresApproval; - + const { address } = useAccount(); return useMutation({ @@ -312,6 +317,9 @@ export const useParaswapSwapCallback = ( if (!optimalRate) { throw new Error("No optimal rate found"); } + if (!wToken) { + throw new Error("WToken not found"); + } try { updateSwapProgressState({ swapStatus: SwapStatus.LOADING }); @@ -326,7 +334,7 @@ export const useParaswapSwapCallback = ( updateSwapProgressState({ currentStep: SwapSteps.Approve }); await approveAllowance( address, - optimalRate.srcToken, + isNativeAddress(inToken.address) ? wToken : inToken.address, optimalRate.tokenTransferProxy as Address ); } @@ -453,7 +461,7 @@ async function signTransaction(quote: Quote, analyticsEvents: AnalyticsEvents) { // Sign transaction and get signature const signature = await promiseWithTimeout( - signTypedData(wagmiConfig, payload), + (signTypedData as any)(wagmiConfig, payload), 40_000 ); @@ -476,9 +484,11 @@ export function useLiquidityHubSwapCallback( ) { const { sdk: liquidityHub, - state: { inToken, slippage }, + state: { inToken }, updateState, } = useLiquidityHubSwapContext(); + const { slippage } = useAppState(); + const buildParaswapTxCallback = useParaswapBuildTxCallback(); const optimalRate = useOptimalRate().data; const { getLatestQuote, data: quote } = useLiquidityHubQuote(); diff --git a/src/trade/liquidity-hub/liquidity-hub-swap.tsx b/src/trade/liquidity-hub/liquidity-hub-swap.tsx index f9780d3..9f8322f 100644 --- a/src/trade/liquidity-hub/liquidity-hub-swap.tsx +++ b/src/trade/liquidity-hub/liquidity-hub-swap.tsx @@ -6,24 +6,9 @@ import { Button } from "@/components/ui/button"; import { _TypedDataEncoder } from "@ethersproject/hash"; import { SwapStatus } from "@orbs-network/swap-ui"; import { Token } from "@/types"; -import { - ErrorCodes, - format, - getLiquidityProviderName, - getMinAmountOut, - getQuoteErrorMessage, - toExactAmount, -} from "@/lib"; +import { ErrorCodes } from "@/lib"; import "../style.css"; import { useConnectModal } from "@rainbow-me/rainbowkit"; -import { SettingsIcon } from "lucide-react"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; import BN from "bignumber.js"; import { LiquidityHubSwapProvider, @@ -37,8 +22,7 @@ import { useOptimalRate, useParaswapMinAmountOut, } from "./hooks"; -import { DataDetails } from "@/components/ui/data-details"; -import { Separator } from "@radix-ui/react-dropdown-menu"; +import { SwapDetails } from "./swap-details"; export const useIsLiquidityHubTrade = () => { const { @@ -47,8 +31,6 @@ export const useIsLiquidityHubTrade = () => { const liquidityHubQuote = useLiquidityHubQuote().data; const paraswapMinAmountOut = useParaswapMinAmountOut(); - console.log(liquidityHubQuote?.minAmountOut, paraswapMinAmountOut); - return useMemo(() => { // Choose between liquidity hub and dex swap based on the min amount out if (liquidityHubDisabled) return false; @@ -64,28 +46,11 @@ export const useIsLiquidityHubTrade = () => { }; function SwapPanel() { - const { - state: { inToken, outToken }, - updateState, - } = useLiquidityHubSwapContext(); - - // Handle Token Switch - const handleSwitch = useCallback(() => { - updateState({ - inToken: outToken, - outToken: inToken, - inputAmount: "", - }); - }, [inToken, outToken, updateState]); - return (
-
-
- -
+ @@ -94,40 +59,23 @@ function SwapPanel() { ); } -const Settings = () => { +const Switch = () => { const { - state: { slippage }, + state: { inToken, outToken }, updateState, } = useLiquidityHubSwapContext(); + + const handleSwitch = useCallback(() => { + updateState({ + inToken: outToken, + outToken: inToken, + inputAmount: "", + }); + }, [inToken, outToken, updateState]); + 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" - /> -
%
-
-
-
-
-
+
+
); }; @@ -138,6 +86,7 @@ const ConfirmationModal = () => { const { data: liquidityHubQuote } = useLiquidityHubQuote(); const { data: optimalRate, isLoading: optimalRateLoading } = useOptimalRate(); const inputError = useLiquidityHubInputError(); + const isLiquidityHubTrade = useIsLiquidityHubTrade(); const { state: { inputAmount }, @@ -157,11 +106,6 @@ const ConfirmationModal = () => { text: "Enter amount", }; } - if (inputAmount && optimalRateLoading) { - return { - text: "Fetching quote...", - }; - } if (!optimalRate && inputAmount) { return { @@ -191,7 +135,7 @@ const ConfirmationModal = () => { setIsOpen(false); updateState({ isLiquidityHubTrade: false }); if (status === SwapStatus.SUCCESS) { - updateState({inputAmount: ''}); + updateState({ inputAmount: "" }); } }, [resetState] @@ -215,61 +159,6 @@ const ConfirmationModal = () => { ); }; -export function SwapDetails() { - const isLiquidityHubTrade = useIsLiquidityHubTrade(); - const optimalRate = useOptimalRate().data; - const account = useAccount().address; - const { - 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 - ); - - const outPriceUsd = useMemo(() => { - if (!optimalRate) return 0; - const amount = toExactAmount(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}`, - }; - - data = { - ...data, - "Est. Received": `${format.crypto(Number(outAmount))} ${outToken.symbol}`, - "Min. Received": `${format.crypto(Number(minOutAmount))} ${ - outToken.symbol - }`, - "Routing source": getLiquidityProviderName(isLiquidityHubTrade), - }; - - return ( -
- - -
-
Recepient
-
{format.address(account)}
-
-
- ); -} - const InTokenCard = () => { const { state: { inputAmount, inToken }, @@ -336,7 +225,7 @@ const OutTokenCard = () => { ); }; -export const Swap = () => { +export const SwapLiquidityHub = () => { return ( diff --git a/src/trade/liquidity-hub/swap-details.tsx b/src/trade/liquidity-hub/swap-details.tsx new file mode 100644 index 0000000..059eb23 --- /dev/null +++ b/src/trade/liquidity-hub/swap-details.tsx @@ -0,0 +1,64 @@ +import { DataDetails } from "@/components/ui/data-details"; +import { toExactAmount, getLiquidityProviderName, format } from "@/lib"; +import { Separator } from "@radix-ui/react-dropdown-menu"; +import { useMemo } from "react"; +import { useAccount } from "wagmi"; +import { useToExactAmount } from "../hooks"; +import { useLiquidityHubSwapContext } from "./context"; +import { useOptimalRate, useParaswapMinAmountOut } from "./hooks"; +import { useIsLiquidityHubTrade } from "./liquidity-hub-swap"; + + export function SwapDetails() { + const isLiquidityHubTrade = useIsLiquidityHubTrade(); + const optimalRate = useOptimalRate().data; + const account = useAccount().address; + const { + 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 + ); + + const outPriceUsd = useMemo(() => { + if (!optimalRate) return 0; + const amount = toExactAmount(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}`, + }; + + data = { + ...data, + "Est. Received": `${format.crypto(Number(outAmount))} ${outToken.symbol}`, + "Min. Received": `${format.crypto(Number(minOutAmount))} ${ + outToken.symbol + }`, + "Routing source": getLiquidityProviderName(isLiquidityHubTrade), + }; + + return ( +
+ + +
+
Recepient
+
{format.address(account)}
+
+
+ ); + } \ No newline at end of file diff --git a/src/trade/settings.tsx b/src/trade/settings.tsx new file mode 100644 index 0000000..7f93379 --- /dev/null +++ b/src/trade/settings.tsx @@ -0,0 +1,43 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { useAppState } from "@/store"; +import { SettingsIcon } from "lucide-react"; + +export const Settings = () => { + const { slippage, setSlippage } = useAppState(); + 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" + /> +
%
+
+
+
+
+
+
+ ); +}; diff --git a/src/trade/trade.tsx b/src/trade/trade.tsx deleted file mode 100644 index aa8f880..0000000 --- a/src/trade/trade.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { Swap } from './liquidity-hub/liquidity-hub-swap' -import { Twap } from './twap/twap' - -export function Trade() { - return ( -
-

Trade

- - - - Swap - TWAP - Limit - - - - - - - - - - - -
- ) -} diff --git a/src/trade/twap/components/inputs.tsx b/src/trade/twap/components/inputs.tsx index e5819f4..6908c5f 100644 --- a/src/trade/twap/components/inputs.tsx +++ b/src/trade/twap/components/inputs.tsx @@ -12,7 +12,7 @@ import { TimeUnit } from "@orbs-network/twap-sdk"; import { useMemo } from "react"; import { NumericFormat } from "react-number-format"; import { useDerivedTwapSwapData } from "../hooks"; -import { useTwapContext } from "../twap-context"; +import { useTwapContext } from "../context"; const options: { text: string; unit: TimeUnit }[] = [ { diff --git a/src/trade/twap/components/limit-price-input.tsx b/src/trade/twap/components/limit-price-input.tsx index 0a17a62..2357ede 100644 --- a/src/trade/twap/components/limit-price-input.tsx +++ b/src/trade/twap/components/limit-price-input.tsx @@ -2,11 +2,9 @@ import { Token } from "@/types"; import { NumericFormat } from "react-number-format"; import { format, - networks, toExactAmount, toRawAmount, usePriceUsd, - useTokensWithBalances, } from "@/lib"; import { Card } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -16,7 +14,7 @@ import { Button } from "@/components/ui/button"; import BN from "bignumber.js"; import { ArrowUpDown, X } from "lucide-react"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; -import { useTwapContext } from "../twap-context"; +import { useTwapContext } from "../context"; import { useMarketPrice, useTradePrice } from "../hooks"; const useMarketPriceUI = () => { diff --git a/src/trade/twap/components/price-toggle.tsx b/src/trade/twap/components/price-toggle.tsx index 32c9c14..6fdb938 100644 --- a/src/trade/twap/components/price-toggle.tsx +++ b/src/trade/twap/components/price-toggle.tsx @@ -1,6 +1,6 @@ import { Switch } from "@/components/ui/switch"; import{ useCallback } from "react"; -import { useTwapContext } from "../twap-context"; +import { useTwapContext } from "../context"; export function PriceToggle() { const { diff --git a/src/trade/twap/components/src-chunk-size.tsx b/src/trade/twap/components/src-chunk-size.tsx index 7866f23..3c7d472 100644 --- a/src/trade/twap/components/src-chunk-size.tsx +++ b/src/trade/twap/components/src-chunk-size.tsx @@ -1,7 +1,7 @@ import { usePriceUsd } from "@/lib"; import { useToExactAmount } from "@/trade/hooks"; import { useDerivedTwapSwapData } from "../hooks"; -import { useTwapContext } from "../twap-context"; +import { useTwapContext } from "../context"; export function SrcChunkSize() { const { state } = useTwapContext(); diff --git a/src/trade/twap/twap-context.tsx b/src/trade/twap/context.tsx similarity index 89% rename from src/trade/twap/twap-context.tsx rename to src/trade/twap/context.tsx index 29a9e53..a73686f 100644 --- a/src/trade/twap/twap-context.tsx +++ b/src/trade/twap/context.tsx @@ -1,3 +1,4 @@ +import { useDefaultTokens } from "@/lib"; import { Token } from "@/types"; import { Configs, @@ -61,12 +62,24 @@ type TwapState = { const initialState = {typedAmount: ''} as TwapState; const useTwapState = () => { - const [values, dispatch] = useReducer( + const [_values, dispatch] = useReducer( (state: TwapState, action: Action) => reducer(state, action, initialState), initialState ); + const defaultTokens = useDefaultTokens(); + + + const values = useMemo(() => { + return { + ..._values, + inToken: _values.inToken || defaultTokens?.inToken || null, + outToken: _values.outToken || defaultTokens?.outToken || null, + } + }, [_values, defaultTokens?.inToken, defaultTokens?.outToken]) + + const updateState = useCallback( (payload: Partial) => { dispatch({ type: "UPDATE_STATE", payload }); diff --git a/src/trade/twap/hooks.ts b/src/trade/twap/hooks.ts index d2c323f..d012b23 100644 --- a/src/trade/twap/hooks.ts +++ b/src/trade/twap/hooks.ts @@ -5,7 +5,7 @@ import { usePriceUsd, } from "@/lib"; import { useMemo } from "react"; -import { useTwapContext } from "./twap-context"; +import { useTwapContext } from "./context"; import BN from "bignumber.js"; import { useToExactAmount, useToRawAmount } from "../hooks"; import { diff --git a/src/trade/twap/orders/orders.tsx b/src/trade/twap/orders/orders.tsx index d346732..fb24845 100644 --- a/src/trade/twap/orders/orders.tsx +++ b/src/trade/twap/orders/orders.tsx @@ -18,7 +18,7 @@ import { eqIgnoreCase, format, makeElipsisAddress, - useTokensList, + useSortedTokens, wagmiConfig, waitForConfirmations, } from "@/lib"; @@ -33,7 +33,7 @@ import { useGroupedOrders, useOrdersQuery } from "./use-orders-query"; import { useExplorer, useToExactAmount } from "@/trade/hooks"; import moment from "moment"; import { useMutation } from "@tanstack/react-query"; -import { useTwapContext } from "../twap-context"; +import { useTwapContext } from "../context"; import { writeContract, simulateContract, @@ -118,11 +118,11 @@ const OrdersMenu = () => { }; const useToken = (tokenAddress?: string) => { - const { data } = useTokensList(); + const tokens = useSortedTokens() return useMemo( - () => data?.find((it) => eqIgnoreCase(it.address, tokenAddress || "")), - [data, tokenAddress] + () => tokens?.find((it) => eqIgnoreCase(it.address, tokenAddress || "")), + [tokens, tokenAddress] ); }; diff --git a/src/trade/twap/orders/use-orders-query.ts b/src/trade/twap/orders/use-orders-query.ts index 8bf6649..217feb2 100644 --- a/src/trade/twap/orders/use-orders-query.ts +++ b/src/trade/twap/orders/use-orders-query.ts @@ -2,7 +2,7 @@ import { groupOrdersByStatus } from "@orbs-network/twap-sdk"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMemo } from "react"; import { useAccount } from "wagmi"; -import { useTwapContext } from "../twap-context"; +import { useTwapContext } from "../context"; const useOrdersQueryKey = () => { const { address } = useAccount(); diff --git a/src/trade/twap/twap-confirmation-dialog.tsx b/src/trade/twap/twap-confirmation-dialog.tsx index bf162a4..46afa3c 100644 --- a/src/trade/twap/twap-confirmation-dialog.tsx +++ b/src/trade/twap/twap-confirmation-dialog.tsx @@ -8,7 +8,7 @@ import { useInTokenUsd, useOutTokenUsd, } from "./hooks"; -import { useTwapContext } from "./twap-context"; +import { useTwapContext } from "./context"; import { format, resolveNativeTokenAddress, diff --git a/src/trade/twap/twap.tsx b/src/trade/twap/twap.tsx index 5b14fb7..0ce5a53 100644 --- a/src/trade/twap/twap.tsx +++ b/src/trade/twap/twap.tsx @@ -3,27 +3,15 @@ import { SwitchButton } from "@/components/ui/switch-button"; import { useCallback, useState } from "react"; import { useAccount } from "wagmi"; import { Button } from "@/components/ui/button"; -import { - useDefaultTokens, - useTokensWithBalances, - useTokenBalance, -} from "@/lib"; +import { useTokenBalaces } from "@/lib"; import "../style.css"; import { useConnectModal } from "@rainbow-me/rainbowkit"; -import { SettingsIcon } from "lucide-react"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; import { LimitPriceInput } from "./components/limit-price-input"; import { TwapContextProvider, useTwapContext, useTwapStateActions, -} from "./twap-context"; +} from "./context"; import { useDerivedTwapSwapData, useInputLabels, @@ -40,29 +28,17 @@ import { SwapStatus } from "@orbs-network/swap-ui"; import { Orders } from "./orders/orders"; export function Panel() { - const { tokensWithBalances, refetch: refetchBalances } = - useTokensWithBalances(); - const [slippage, setSlippage] = useState(0.5); + const { refetch: refetchBalances } = useTokenBalaces(); // Get wagmi account const account = useAccount(); const { state } = useTwapContext(); const { - updateState, resetState, values: { inToken, outToken, typedAmount }, } = state; - // Set Initial Tokens - const defaultTokens = useDefaultTokens({ - inToken, - outToken, - tokensWithBalances, - setInToken: (token) => updateState({ inToken: token }), - setOutToken: (token) => updateState({ outToken: token }), - }); - const inputError = useTwapInputError(); const { setInToken, setInputAmount, setOutToken, onSwitchTokens } = useTwapStateActions(); @@ -92,33 +68,6 @@ export function Panel() { const amountLoading = Boolean(inToken && !marketPrice); 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" - /> -
%
-
-
-
-
-
-
@@ -126,7 +75,7 @@ export function Panel() { label={inputLabel} amount={typedAmount} amountUsd={inAmountUsd} - selectedToken={inToken || defaultTokens[0]} + selectedToken={inToken} onSelectToken={setInToken} onValueChange={setInputAmount} inputError={inputError} @@ -138,7 +87,7 @@ export function Panel() { label={outputLabel} amount={destAmount ?? ""} amountUsd={outAmountUsd} - selectedToken={outToken || defaultTokens[1]} + selectedToken={outToken} onSelectToken={setOutToken} isAmountEditable={false} amountLoading={amountLoading} @@ -167,10 +116,15 @@ export function Panel() { ); } -export const Twap = ({ isLimitPanel }: { isLimitPanel?: boolean }) => { +export const SwapTwap = ({ isLimitPanel }: { isLimitPanel?: boolean }) => { return ( ); }; + + +export const SwapLimit = () => { + return +} \ No newline at end of file diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo index f7a97e0..c14af47 100644 --- a/tsconfig.app.tsbuildinfo +++ b/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/root.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/header.tsx","./src/components/order-details.tsx","./src/components/spinner.tsx","./src/components/swap-details.tsx","./src/components/theme-toggle.tsx","./src/components/tokens/token-card.tsx","./src/components/tokens/token-select.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/data-details.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch-button.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/lib/abis.ts","./src/lib/approveallowance.ts","./src/lib/getrequiresapproval.ts","./src/lib/index.ts","./src/lib/networks.ts","./src/lib/usebalances.ts","./src/lib/usedebounce.ts","./src/lib/usedefaulttokens.ts","./src/lib/usegetrequiresapproval.ts","./src/lib/usehandleinputerror.ts","./src/lib/useparaswap.ts","./src/lib/usepriceusd.ts","./src/lib/usetokenlist.ts","./src/lib/usetokenswithbalances.ts","./src/lib/usewraporunwraponly.ts","./src/lib/utils.ts","./src/lib/wagmi-config.ts","./src/lib/wraptoken.ts","./src/providers/rainbow-provider.tsx","./src/providers/theme-provider.tsx","./src/trade/hooks.ts","./src/trade/swap-confirmation-dialog.tsx","./src/trade/trade.tsx","./src/trade/use-swap-state.ts","./src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx","./src/trade/liquidity-hub/liquidity-hub-swap.tsx","./src/trade/liquidity-hub/useliquidityhubquote.ts","./src/trade/liquidity-hub/useliquidityhubsdk.ts","./src/trade/liquidity-hub/useliquidityhubswapcallback.ts","./src/trade/twap/hooks.ts","./src/trade/twap/twap-confirmation-dialog.tsx","./src/trade/twap/twap-context.tsx","./src/trade/twap/twap.tsx","./src/trade/twap/utils.ts","./src/trade/twap/components/inputs.tsx","./src/trade/twap/components/limit-price-input.tsx","./src/trade/twap/components/price-toggle.tsx","./src/trade/twap/components/src-chunk-size.tsx","./src/trade/twap/orders/orders-context.tsx","./src/trade/twap/orders/orders.tsx","./src/trade/twap/orders/use-orders-query.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/root.tsx","./src/store.ts","./src/types.ts","./src/vite-env.d.ts","./src/components/header.tsx","./src/components/order-details.tsx","./src/components/spinner.tsx","./src/components/theme-toggle.tsx","./src/components/tokens/token-card.tsx","./src/components/tokens/token-select.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/data-details.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch-button.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/lib/abis.ts","./src/lib/approveallowance.ts","./src/lib/getrequiresapproval.ts","./src/lib/index.ts","./src/lib/networks.ts","./src/lib/usedebounce.ts","./src/lib/usedefaulttokens.ts","./src/lib/usegetrequiresapproval.ts","./src/lib/usehandleinputerror.ts","./src/lib/useparaswap.ts","./src/lib/usepriceusd.ts","./src/lib/usetokens.ts","./src/lib/usewraporunwraponly.ts","./src/lib/utils.ts","./src/lib/wagmi-config.ts","./src/lib/wraptoken.ts","./src/providers/rainbow-provider.tsx","./src/providers/theme-provider.tsx","./src/trade/hooks.ts","./src/trade/settings.tsx","./src/trade/swap-confirmation-dialog.tsx","./src/trade/liquidity-hub/context.tsx","./src/trade/liquidity-hub/hooks.ts","./src/trade/liquidity-hub/liquidity-hub-confirmation-dialog.tsx","./src/trade/liquidity-hub/liquidity-hub-swap.tsx","./src/trade/liquidity-hub/swap-details.tsx","./src/trade/twap/context.tsx","./src/trade/twap/hooks.ts","./src/trade/twap/twap-confirmation-dialog.tsx","./src/trade/twap/twap.tsx","./src/trade/twap/utils.ts","./src/trade/twap/components/inputs.tsx","./src/trade/twap/components/limit-price-input.tsx","./src/trade/twap/components/price-toggle.tsx","./src/trade/twap/components/src-chunk-size.tsx","./src/trade/twap/orders/orders-context.tsx","./src/trade/twap/orders/orders.tsx","./src/trade/twap/orders/use-orders-query.ts"],"version":"5.6.3"} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 883989b..156aa9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -983,10 +983,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@orbs-network/liquidity-hub-sdk@^1.0.41": - version "1.0.41" - resolved "https://registry.yarnpkg.com/@orbs-network/liquidity-hub-sdk/-/liquidity-hub-sdk-1.0.41.tgz#9d19adf60df4a546217eb2f08f9691856bd8416e" - integrity sha512-dYDtvCwYI6y0zXYQs8VgrlR+XlFyxgG4a+FOtOHcHCX8ZlPyp7DiwppFl9ly4qMEKM0IFxlnny1fyOTWAHYmuA== +"@orbs-network/liquidity-hub-sdk@^1.0.44": + version "1.0.44" + resolved "https://registry.yarnpkg.com/@orbs-network/liquidity-hub-sdk/-/liquidity-hub-sdk-1.0.44.tgz#244c4e26bbeef75d48a8ca041fa730c5885e6a17" + integrity sha512-5IpPDNUVdnzv2+4MdLDFHjJQfPqn+NWj1EqBcdKgZ3YLELwYrgJ8O+38Tuy7iU1fS/OR07f7BvRyczywkGPRJA== "@orbs-network/swap-ui@^0.0.14": version "0.0.14"