From a78c026a16dca7eadcf107536365a828e808099b Mon Sep 17 00:00:00 2001 From: yjin Date: Mon, 23 Dec 2024 18:31:27 +0900 Subject: [PATCH 1/5] feat: [GSW-1984] Swap Value Debounce --- .../src/hooks/swap/data/use-swap-handler.tsx | 7 ++- packages/web/src/hooks/swap/data/use-swap.tsx | 62 ++++++++++++++----- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/packages/web/src/hooks/swap/data/use-swap-handler.tsx b/packages/web/src/hooks/swap/data/use-swap-handler.tsx index c539e34fe..7147d2a2b 100644 --- a/packages/web/src/hooks/swap/data/use-swap-handler.tsx +++ b/packages/web/src/hooks/swap/data/use-swap-handler.tsx @@ -169,6 +169,7 @@ export const useSwapHandler = () => { unwrap, updateSwapAmount, resetSwapAmount, + isTyping, } = useSwap({ tokenA, tokenB, @@ -300,7 +301,7 @@ export const useSwapHandler = () => { ); prevPriceImpact.current = BigNumber(priceImpactNum.toFixed(2)); return BigNumber(priceImpactNum.toFixed(2)); - }, [estimatedRoutes, swapFee, tokenA, tokenAAmount, tokenB, tokenBAmount, tokenPrices]); + }, [estimatedRoutes, swapFee, tokenA?.path, tokenAAmount, tokenB?.path, tokenBAmount, tokenPrices]); const priceImpactStatus: PriceImpactStatus = useMemo(() => { if (!priceImpact) return "NONE"; @@ -1033,7 +1034,7 @@ export const useSwapHandler = () => { } if (swapState !== "SUCCESS" && estimatedAmount === null) { - if (swapState === "NO_LIQUIDITY") { + if (swapState === "NO_LIQUIDITY" || swapState === "NONE") { if (type === "EXACT_IN") { setTokenBAmount(""); } else { @@ -1106,7 +1107,7 @@ export const useSwapHandler = () => { executeSwap, isSwitchNetwork, switchNetwork, - isLoading: swapState === "LOADING", + isLoading: swapState === "LOADING" || isTyping, setSwapValue, tokenA, tokenB, diff --git a/packages/web/src/hooks/swap/data/use-swap.tsx b/packages/web/src/hooks/swap/data/use-swap.tsx index 144d57c97..e3e905596 100644 --- a/packages/web/src/hooks/swap/data/use-swap.tsx +++ b/packages/web/src/hooks/swap/data/use-swap.tsx @@ -1,12 +1,14 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import BigNumber from "bignumber.js"; + import { SwapDirectionType } from "@common/values"; import { useGnoswapContext } from "@hooks/common/use-gnoswap-context"; import { useWallet } from "@hooks/wallet/data/use-wallet"; import { TokenModel, isNativeToken } from "@models/token/token-model"; import { EstimatedRoute } from "@models/swap/swap-route-info"; import { makeDisplayTokenAmount } from "@utils/token-utils"; -import BigNumber from "bignumber.js"; -import { useCallback, useEffect, useMemo, useState } from "react"; import { useGetRoutes } from "@query/router"; +import useDebounce from "@hooks/common/use-debounce"; interface UseSwapProps { tokenA: TokenModel | null; @@ -20,7 +22,16 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U const { account } = useWallet(); const { swapRouterRepository } = useGnoswapContext(); const [swapAmount, setSwapAmount] = useState(null); + const debouncedAmount = useDebounce(swapAmount, 500); const [estimatedLiquidityMax, setEstimatedLiquidityMax] = useState(null); + const [isTyping, setIsTyping] = useState(false); + + const debouncedSwapAmount = useMemo(() => { + if (!swapAmount || swapAmount === 0) { + return swapAmount; + } + return debouncedAmount; + }, [swapAmount, debouncedAmount]); const shouldFetchData = useCallback( (amount: number | null) => { @@ -30,7 +41,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U }, [estimatedLiquidityMax], ); - const shouldFetch = shouldFetchData(swapAmount); + const shouldFetch = shouldFetchData(debouncedSwapAmount); const selectedTokenPair = tokenA !== null && tokenB !== null; @@ -49,7 +60,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U return false; }, [tokenA, tokenB]); - const hasValidSwapAmount = Boolean(swapAmount && swapAmount > 0); + const hasValidSwapAmount = Boolean(debouncedSwapAmount && debouncedSwapAmount > 0); const hasValidTokenPaths = Boolean(tokenA?.path) && Boolean(tokenB?.path); const isDifferentTokens = !isSameToken; @@ -64,7 +75,12 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U inputToken: tokenA, outputToken: tokenB, exactType: direction, - tokenAmount: direction === "EXACT_IN" ? swapAmount : swapAmount ? swapAmount * exactOutPadding : swapAmount, + tokenAmount: + direction === "EXACT_IN" + ? debouncedSwapAmount + : debouncedSwapAmount + ? debouncedSwapAmount * exactOutPadding + : debouncedSwapAmount, }, { enabled: isEnabledQuery, @@ -72,7 +88,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U ); const swapState: "NONE" | "LOADING" | "NO_LIQUIDITY" | "SUCCESS" = useMemo(() => { - if (!selectedTokenPair || !swapAmount) { + if (!selectedTokenPair || !debouncedSwapAmount) { return "NONE"; } @@ -89,10 +105,10 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U } return "SUCCESS"; - }, [swapAmount, error, estimatedSwapResult?.amount, isEstimatedSwapLoading, isSameToken, selectedTokenPair]); + }, [debouncedSwapAmount, error, estimatedSwapResult?.amount, isEstimatedSwapLoading, isSameToken, selectedTokenPair]); const estimatedRoutes: EstimatedRoute[] | null = useMemo(() => { - if (swapState === "LOADING" || !swapAmount) { + if (swapState === "LOADING" || !debouncedSwapAmount || isTyping) { return null; } @@ -101,14 +117,14 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U } return estimatedSwapResult.estimatedRoutes; - }, [swapState, estimatedSwapResult, swapAmount]); + }, [swapState, estimatedSwapResult, debouncedSwapAmount, isTyping]); const estimatedAmount: string | null = useMemo(() => { if (!tokenA || !tokenB) { return null; } - if (!swapAmount || error) { + if (!debouncedSwapAmount || error || isTyping) { return null; } @@ -120,7 +136,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U return direction === "EXACT_IN" ? makeDisplayTokenAmount(tokenB, amount)?.toString() || null : makeDisplayTokenAmount(tokenA, amount)?.toString() || null; - }, [swapAmount, error, swapState, estimatedSwapResult]); + }, [debouncedSwapAmount, error, swapState, estimatedSwapResult, isTyping]); const tokenAmountLimit = useMemo(() => { if (estimatedAmount && !Number.isNaN(slippage)) { @@ -137,13 +153,17 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U return 0; } - return tokenA ? tokenAmountLimit || 0 : 0; + return tokenA ? makeDisplayTokenAmount(tokenA, tokenAmountLimit) || 0 : 0; } return 0; }, [direction, estimatedAmount, slippage, tokenA]); const updateSwapAmount = useCallback((amount: string) => { - if (!amount) return setSwapAmount(null); + if (!amount) { + setSwapAmount(null); + setIsTyping(false); + return; + } let newAmount = 0; if (BigNumber(amount).isZero()) { @@ -152,8 +172,15 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U newAmount = BigNumber(amount).toNumber(); setSwapAmount(newAmount); + setIsTyping(true); }, []); + useEffect(() => { + if (debouncedSwapAmount !== null) { + setIsTyping(false); + } + }, [debouncedSwapAmount]); + const wrap = useCallback( async (tokenAmount: string) => { if (!account) { @@ -212,14 +239,14 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U if (estimatedRoutes.length === 0) { if (!estimatedLiquidityMax) { - setEstimatedLiquidityMax(swapAmount || null); - } else if (swapAmount && swapAmount < estimatedLiquidityMax) { - setEstimatedLiquidityMax(swapAmount); + setEstimatedLiquidityMax(debouncedSwapAmount || null); + } else if (debouncedSwapAmount && debouncedSwapAmount < estimatedLiquidityMax) { + setEstimatedLiquidityMax(debouncedSwapAmount); } } else { setEstimatedLiquidityMax(null); } - }, [estimatedRoutes, swapAmount, estimatedLiquidityMax]); + }, [estimatedRoutes, debouncedSwapAmount, estimatedLiquidityMax]); /** * Reset estimatedLiquidityMax to null after specified delay @@ -247,6 +274,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U unwrap, updateSwapAmount, isEstimatedSwapLoading, + isTyping, resetSwapAmount: () => setSwapAmount(0), }; }; From 83f6f3af6ea3eff9d6bc1ea0441e31dd8953bc17 Mon Sep 17 00:00:00 2001 From: yjin Date: Mon, 23 Dec 2024 21:33:35 +0900 Subject: [PATCH 2/5] fix: SwapSummaryInfo UI (u-units) --- packages/web/src/hooks/swap/data/use-swap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/hooks/swap/data/use-swap.tsx b/packages/web/src/hooks/swap/data/use-swap.tsx index e3e905596..ed2a653ee 100644 --- a/packages/web/src/hooks/swap/data/use-swap.tsx +++ b/packages/web/src/hooks/swap/data/use-swap.tsx @@ -153,7 +153,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U return 0; } - return tokenA ? makeDisplayTokenAmount(tokenA, tokenAmountLimit) || 0 : 0; + return tokenA ? tokenAmountLimit || 0 : 0; } return 0; }, [direction, estimatedAmount, slippage, tokenA]); From 53d55113771ec7af3851fe2a89c038ca89aef7d6 Mon Sep 17 00:00:00 2001 From: yjin Date: Mon, 23 Dec 2024 22:09:37 +0900 Subject: [PATCH 3/5] refactor: [SonarQube] Ternary operators should not be nested Extract this nested ternary operation into an independent statement. Ternary operators should not be nested typescript:S3358 Software qualities impacted: Maintainability --- packages/web/src/hooks/swap/data/use-swap.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/web/src/hooks/swap/data/use-swap.tsx b/packages/web/src/hooks/swap/data/use-swap.tsx index ed2a653ee..58f269d5f 100644 --- a/packages/web/src/hooks/swap/data/use-swap.tsx +++ b/packages/web/src/hooks/swap/data/use-swap.tsx @@ -66,6 +66,14 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U const isEnabledQuery = shouldFetch && hasValidSwapAmount && hasValidTokenPaths && isDifferentTokens; + const getTokenAmount = useMemo(() => { + if (direction === "EXACT_IN") { + return debouncedSwapAmount; + } + + return debouncedSwapAmount ? debouncedSwapAmount * exactOutPadding : debouncedSwapAmount; + }, [debouncedSwapAmount, direction, exactOutPadding]); + const { data: estimatedSwapResult, isLoading: isEstimatedSwapLoading, @@ -75,12 +83,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U inputToken: tokenA, outputToken: tokenB, exactType: direction, - tokenAmount: - direction === "EXACT_IN" - ? debouncedSwapAmount - : debouncedSwapAmount - ? debouncedSwapAmount * exactOutPadding - : debouncedSwapAmount, + tokenAmount: getTokenAmount, }, { enabled: isEnabledQuery, From 1b48d3e2210ad5c8846915caa435902d3c1e0062 Mon Sep 17 00:00:00 2001 From: yjin Date: Thu, 26 Dec 2024 14:39:50 +0900 Subject: [PATCH 4/5] fix: MissingLogo UI (font-size) --- .../src/components/common/missing-logo/MissingLogo.styles.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/components/common/missing-logo/MissingLogo.styles.tsx b/packages/web/src/components/common/missing-logo/MissingLogo.styles.tsx index cdbf09cdb..48fa24043 100644 --- a/packages/web/src/components/common/missing-logo/MissingLogo.styles.tsx +++ b/packages/web/src/components/common/missing-logo/MissingLogo.styles.tsx @@ -64,12 +64,12 @@ export const LogoWrapper = styled.div` font-weight: 600; font-size: ${({ width, placeholderFontSize }) => { if (placeholderFontSize) return `${placeholderFontSize}px`; - return `${getFontSize(width)}`; + return `${getFontSize(width)}px`; }}; ${media.mobile} { font-size: ${({ mobileWidth, placeholderFontSize }) => { if (placeholderFontSize) return `${placeholderFontSize}px`; - return `${getFontSize(mobileWidth)}`; + return `${getFontSize(mobileWidth)}px`; }}; height: ${({ mobileWidth }) => { return `${mobileWidth}px`; From 3864259be0173a7db373cee614477b1958b268fb Mon Sep 17 00:00:00 2001 From: yjin Date: Thu, 26 Dec 2024 21:06:12 +0900 Subject: [PATCH 5/5] fix: [GSW-1984] Improve State management (User now typing) --- .../src/hooks/swap/data/use-swap-handler.tsx | 4 +- packages/web/src/hooks/swap/data/use-swap.tsx | 59 +++++++++++++------ 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/packages/web/src/hooks/swap/data/use-swap-handler.tsx b/packages/web/src/hooks/swap/data/use-swap-handler.tsx index 7147d2a2b..d4231894d 100644 --- a/packages/web/src/hooks/swap/data/use-swap-handler.tsx +++ b/packages/web/src/hooks/swap/data/use-swap-handler.tsx @@ -593,7 +593,9 @@ export const useSwapHandler = () => { const changeTokenAAmount = useCallback( (changed: string, none?: boolean) => { const value = handleAmount(changed, tokenA); - updateSwapAmount(value); + if (tokenA && tokenB) { + updateSwapAmount(value); + } if (isSameToken) { setTokenAAmount(value); diff --git a/packages/web/src/hooks/swap/data/use-swap.tsx b/packages/web/src/hooks/swap/data/use-swap.tsx index 58f269d5f..ad4adee92 100644 --- a/packages/web/src/hooks/swap/data/use-swap.tsx +++ b/packages/web/src/hooks/swap/data/use-swap.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import BigNumber from "bignumber.js"; import { SwapDirectionType } from "@common/values"; @@ -21,10 +21,13 @@ interface UseSwapProps { export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: UseSwapProps) => { const { account } = useWallet(); const { swapRouterRepository } = useGnoswapContext(); + + const SWAP_AMOUNT_DEBOUNCE_TIME_MS = 500; const [swapAmount, setSwapAmount] = useState(null); - const debouncedAmount = useDebounce(swapAmount, 500); + const debouncedAmount = useDebounce(swapAmount, SWAP_AMOUNT_DEBOUNCE_TIME_MS); const [estimatedLiquidityMax, setEstimatedLiquidityMax] = useState(null); const [isTyping, setIsTyping] = useState(false); + const typingTimeoutRef = useRef(); const debouncedSwapAmount = useMemo(() => { if (!swapAmount || swapAmount === 0) { @@ -35,11 +38,12 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U const shouldFetchData = useCallback( (amount: number | null) => { + if (!tokenA || !tokenB) return false; if (!amount) return false; if (!estimatedLiquidityMax) return true; return amount < estimatedLiquidityMax; }, - [estimatedLiquidityMax], + [estimatedLiquidityMax, tokenA, tokenB], ); const shouldFetch = shouldFetchData(debouncedSwapAmount); @@ -161,22 +165,35 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U return 0; }, [direction, estimatedAmount, slippage, tokenA]); - const updateSwapAmount = useCallback((amount: string) => { - if (!amount) { - setSwapAmount(null); - setIsTyping(false); - return; - } + const updateSwapAmount = useCallback( + (amount: string) => { + if (!amount) { + setSwapAmount(null); + setIsTyping(false); + return; + } - let newAmount = 0; - if (BigNumber(amount).isZero()) { - newAmount = 0; - } - newAmount = BigNumber(amount).toNumber(); + let newAmount = 0; + if (BigNumber(amount).isZero()) { + newAmount = 0; + } + newAmount = BigNumber(amount).toNumber(); - setSwapAmount(newAmount); - setIsTyping(true); - }, []); + setSwapAmount(newAmount); + if (tokenA && tokenB) { + setIsTyping(true); + } + + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + + typingTimeoutRef.current = setTimeout(() => { + setIsTyping(false); + }, SWAP_AMOUNT_DEBOUNCE_TIME_MS + 100); + }, + [tokenA, tokenB], + ); useEffect(() => { if (debouncedSwapAmount !== null) { @@ -238,7 +255,7 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U ); useEffect(() => { - if (estimatedRoutes === null) return; + if (estimatedRoutes === null || !tokenA || !tokenB) return; if (estimatedRoutes.length === 0) { if (!estimatedLiquidityMax) { @@ -278,6 +295,10 @@ export const useSwap = ({ tokenA, tokenB, direction, slippage, swapFee = 15 }: U updateSwapAmount, isEstimatedSwapLoading, isTyping, - resetSwapAmount: () => setSwapAmount(0), + resetSwapAmount: () => { + setSwapAmount(0); + setIsTyping(false); + setEstimatedLiquidityMax(null); + }, }; };