From bedd0c75752fdefa7b37ffb59ce32786e067b363 Mon Sep 17 00:00:00 2001 From: jinoosss Date: Wed, 13 Dec 2023 21:57:44 +0900 Subject: [PATCH] feat: [GSW-468] Implements Remove Position --- .../my-liquidity-header/MyLiquidityHeader.tsx | 7 +- .../pool/my-liquidity/MyLiquidity.tsx | 5 +- .../RemoveLiquiditySelectListItem.tsx | 126 +++++++++--------- .../RemoveLiquiditySelectList.tsx | 50 +++---- .../RemoveLiquiditySelectResult.tsx | 108 +++------------ .../remove-liquidity/RemoveLiquidity.tsx | 61 ++++----- .../RemovePositionModal.styles.ts | 10 +- .../RemovePositionModal.tsx | 118 +++++++--------- .../SelectLiquidityItem.tsx | 4 +- .../MyLiquidityContainer.tsx | 26 ++-- .../RemoveLiquidityContainer.tsx | 88 +++++++----- .../RemovePositionModalContainer.tsx | 35 ++++- .../hooks/earn/use-remove-position-modal.tsx | 16 ++- .../web/src/hooks/stake/use-remove-data.ts | 98 ++++++++++++++ .../position/position-repository-impl.ts | 39 +++--- .../position/position-repository.ts | 6 +- .../request/decrease-liquidity-request.ts | 2 +- .../request/remove-liquidity-request.ts | 5 + 18 files changed, 438 insertions(+), 366 deletions(-) create mode 100644 packages/web/src/hooks/stake/use-remove-data.ts create mode 100644 packages/web/src/repositories/position/request/remove-liquidity-request.ts diff --git a/packages/web/src/components/pool/my-liquidity-header/MyLiquidityHeader.tsx b/packages/web/src/components/pool/my-liquidity-header/MyLiquidityHeader.tsx index 63ebf7724..869b4935b 100644 --- a/packages/web/src/components/pool/my-liquidity-header/MyLiquidityHeader.tsx +++ b/packages/web/src/components/pool/my-liquidity-header/MyLiquidityHeader.tsx @@ -5,19 +5,18 @@ import React from "react"; import { HeaderWrapper } from "./MyLiquidityHeader.styles"; interface MyLiquidityHeaderProps { - connected: boolean; - isSwitchNetwork: boolean; + availableRemovePosition: boolean; handleClickAddPosition: () => void; handleClickRemovePosition: () => void; } -const MyLiquidityHeader: React.FC = ({ connected, isSwitchNetwork, handleClickAddPosition, handleClickRemovePosition }) => { +const MyLiquidityHeader: React.FC = ({ availableRemovePosition, handleClickAddPosition, handleClickRemovePosition }) => { return (

My Positions

); diff --git a/packages/web/src/components/remove/remove-position-modal/RemovePositionModal.styles.ts b/packages/web/src/components/remove/remove-position-modal/RemovePositionModal.styles.ts index 3916b565c..bb2efe4a8 100644 --- a/packages/web/src/components/remove/remove-position-modal/RemovePositionModal.styles.ts +++ b/packages/web/src/components/remove/remove-position-modal/RemovePositionModal.styles.ts @@ -51,7 +51,7 @@ export const RemovePositionModalWrapper = styled.div` .box-item { width: 100%; ${mixins.flexbox("column", "flex-start", "flex-start")}; - gap: 8px; + gap: 16px; h4 { ${fonts.body12} @@ -150,7 +150,7 @@ export const RemovePositionModalWrapper = styled.div` > div { width: 100%; .button-confirm { - gap: 8px; + gap: 16px; height: 57px; span { ${fonts.body7} @@ -183,8 +183,12 @@ export const RemovePositionModalWrapper = styled.div` } } } -`; + .button-wrapper { + display: flex; + width: 100%; + } +`; export const Divider = styled.div` width: 100%; diff --git a/packages/web/src/components/remove/remove-position-modal/RemovePositionModal.tsx b/packages/web/src/components/remove/remove-position-modal/RemovePositionModal.tsx index 93809c56a..2090a5e5f 100644 --- a/packages/web/src/components/remove/remove-position-modal/RemovePositionModal.tsx +++ b/packages/web/src/components/remove/remove-position-modal/RemovePositionModal.tsx @@ -2,15 +2,20 @@ import Badge, { BADGE_TYPE } from "@components/common/badge/Badge"; import Button, { ButtonHierarchy } from "@components/common/button/Button"; import DoubleLogo from "@components/common/double-logo/DoubleLogo"; import IconClose from "@components/common/icons/IconCancel"; +import { useRemoveData } from "@hooks/stake/use-remove-data"; +import { PoolPositionModel } from "@models/position/pool-position-model"; +import { numberToUSD } from "@utils/number-utils"; import React, { useCallback } from "react"; import { Divider, RemovePositionModalWrapper } from "./RemovePositionModal.styles"; interface Props { + positions: PoolPositionModel[]; close: () => void; onSubmit: () => void; } -const RemovePositionModal: React.FC = ({ close, onSubmit }) => { +const RemovePositionModal: React.FC = ({ positions, close, onSubmit }) => { + const { unclaimedRewards, totalLiquidityUSD } = useRemoveData({ positions }); const onClickClose = useCallback(() => { close(); }, [close]); @@ -28,84 +33,61 @@ const RemovePositionModal: React.FC = ({ close, onSubmit }) => {

Positions

-
-
- -
GNS/GNOT
- -
-
$145,541.10
-
-
-
- -
GNS/GNOT
- -
-
$145,541.10
-
-
-
-
-

Unclaimed Fees

-
-
-
+ {positions.map((position, index) => ( +
- logo -
GNS
+ +
{`${position.pool.tokenA.symbol}/${position.pool.tokenB.symbol}`}
+
-
15,000.005
-
-
- $15,000.01 +
{numberToUSD(Number(position.positionUsdValue))}
+ ))} +
+
+

Unclaimed Fees

+
+ {unclaimedRewards.map((rewardInfo, index) => ( +
+
+
+ logo +
{rewardInfo.token.symbol}
+
+
{rewardInfo.amount}
+
+
{rewardInfo.amountUSD}
+
+ ))}
-
+
+ +
+
-
- logo -
GNS
+
+ Total Amount
-
15,000.005
-
-
- $15,000.01 +
{totalLiquidityUSD}
-
-
- -
-
-
-
- Total Amount -
-
$291,082.2
-
+
+
-
-
diff --git a/packages/web/src/components/unstake/select-liquidity-item/SelectLiquidityItem.tsx b/packages/web/src/components/unstake/select-liquidity-item/SelectLiquidityItem.tsx index 19dfca7bb..e09fe1c89 100644 --- a/packages/web/src/components/unstake/select-liquidity-item/SelectLiquidityItem.tsx +++ b/packages/web/src/components/unstake/select-liquidity-item/SelectLiquidityItem.tsx @@ -58,8 +58,8 @@ const SelectLiquidityItem: React.FC = ({ }, [position.pool.tokenB]); const liquidityUSD = useMemo(() => { - return numberToUSD(Number(position.liquidity)); - }, [position.liquidity]); + return numberToUSD(Number(position.positionUsdValue)); + }, [position.positionUsdValue]); return (
  • diff --git a/packages/web/src/containers/my-liquidity-container/MyLiquidityContainer.tsx b/packages/web/src/containers/my-liquidity-container/MyLiquidityContainer.tsx index 34749b9da..8450c5b1e 100644 --- a/packages/web/src/containers/my-liquidity-container/MyLiquidityContainer.tsx +++ b/packages/web/src/containers/my-liquidity-container/MyLiquidityContainer.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import MyLiquidity from "@components/pool/my-liquidity/MyLiquidity"; import { useWindowSize } from "@hooks/common/use-window-size"; import { useWallet } from "@hooks/wallet/use-wallet"; @@ -19,15 +19,12 @@ const MyLiquidityContainer: React.FC = () => { const { getPositionsByPoolId } = usePositionData(); const { claimAll } = usePosition(positions); - useEffect(() => { - const poolPath = router.query["pool-path"] as string; - if (!poolPath) { - return; + const availableRemovePosition = useMemo(() => { + if (!connectedWallet || isSwitchNetwork) { + return false; } - if (account?.address) { - getPositionsByPoolId(poolPath).then(setPositions); - } - }, [account?.address, getPositionsByPoolId, router.query]); + return positions.length > 0; + }, [connectedWallet, isSwitchNetwork, positions.length]); const handleClickAddPosition = useCallback(() => { router.push(`${router.asPath}/add`); @@ -52,6 +49,16 @@ const MyLiquidityContainer: React.FC = () => { }); }, [claimAll, router]); + useEffect(() => { + const poolPath = router.query["pool-path"] as string; + if (!poolPath) { + return; + } + if (account?.address) { + getPositionsByPoolId(poolPath).then(setPositions); + } + }, [account?.address, getPositionsByPoolId, router.query]); + return ( { onScroll={handleScroll} currentIndex={currentIndex} claimAll={claimAllReward} + availableRemovePosition={availableRemovePosition} /> ); }; diff --git a/packages/web/src/containers/remove-liquidity-container/RemoveLiquidityContainer.tsx b/packages/web/src/containers/remove-liquidity-container/RemoveLiquidityContainer.tsx index dfc14a9fe..82892a95b 100644 --- a/packages/web/src/containers/remove-liquidity-container/RemoveLiquidityContainer.tsx +++ b/packages/web/src/containers/remove-liquidity-container/RemoveLiquidityContainer.tsx @@ -1,59 +1,81 @@ import RemoveLiquidity from "@components/remove/remove-liquidity/RemoveLiquidity"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useRemovePositionModal } from "@hooks/earn/use-remove-position-modal"; -import { useWindowSize } from "@hooks/common/use-window-size"; import { PoolPositionModel } from "@models/position/pool-position-model"; import { usePositionData } from "@hooks/common/use-position-data"; +import { useRouter } from "next/router"; +import { useWallet } from "@hooks/wallet/use-wallet"; const RemoveLiquidityContainer: React.FC = () => { - const [selectedIds, setSelectedIds] = useState([]); - const { width } = useWindowSize(); - const { openModal } = useRemovePositionModal(); + const router = useRouter(); + const { account } = useWallet(); const [positions, setPositions] = useState([]); - const { getPositions } = usePositionData(); + const [checkedList, setCheckedList] = useState([]); + const { getPositionsByPoolId } = usePositionData(); + const { openModal } = useRemovePositionModal({ + positions, + selectedIds: checkedList, + }); - useEffect(() => { - getPositions().then(setPositions); - }, [getPositions]); - - const unstakedLiquidities = useMemo(() => { - return positions.filter(item => item.unclaimedFee0Amount + item.unclaimedFee1Amount > 0); + const stakedPositions = useMemo(() => { + return positions.filter(position => position.staked); }, [positions]); - const selectedAll = useMemo(() => { - return unstakedLiquidities.length === selectedIds.length; - }, [selectedIds.length, unstakedLiquidities.length]); + const unstakedPositions = useMemo(() => { + return positions.filter(position => !position.staked); + }, [positions]); - const selectAll = useCallback(() => { - if (selectedAll) { - setSelectedIds([]); - return; + const checkedAll = useMemo(() => { + if (unstakedPositions.length === 0) { + return false; } - const selectedIds = unstakedLiquidities.map(liquidity => liquidity.id); - setSelectedIds(selectedIds); - }, [selectedAll, unstakedLiquidities]); + return unstakedPositions.length === checkedList.length; + }, [unstakedPositions, checkedList]); + + const onCheckedItem = useCallback( + (isChecked: boolean, path: string) => { + if (isChecked) { + return setCheckedList((prev: string[]) => [...prev, path]); + } + if (!isChecked && checkedList.includes(path)) { + return setCheckedList(checkedList.filter(el => el !== path)); + } + }, + [checkedList], + ); - const select = useCallback((id: string) => { - if (selectedIds.includes(id)) { - setSelectedIds(selectedIds.filter((selectedId => selectedId !== id))); + const onCheckedAll = useCallback(() => { + if (checkedAll) { + setCheckedList([]); return; } - setSelectedIds([...selectedIds, id]); - }, [selectedIds]); + const checkedList = unstakedPositions.map(position => position.id); + setCheckedList(checkedList); + }, [checkedAll, unstakedPositions]); const removeLiquidity = useCallback(() => { openModal(); - }, []); + }, [openModal]); + + useEffect(() => { + const poolPath = router.query["pool-path"] as string; + if (!poolPath) { + return; + } + if (account?.address) { + getPositionsByPoolId(poolPath).then(setPositions); + } + }, [account?.address, getPositionsByPoolId, router.query]); return ( ); }; diff --git a/packages/web/src/containers/remove-position-modal-container/RemovePositionModalContainer.tsx b/packages/web/src/containers/remove-position-modal-container/RemovePositionModalContainer.tsx index 48410a063..b60a05ae7 100644 --- a/packages/web/src/containers/remove-position-modal-container/RemovePositionModalContainer.tsx +++ b/packages/web/src/containers/remove-position-modal-container/RemovePositionModalContainer.tsx @@ -1,19 +1,44 @@ import RemovePositionModal from "@components/remove/remove-position-modal/RemovePositionModal"; import { useClearModal } from "@hooks/common/use-clear-modal"; +import { useGnoswapContext } from "@hooks/common/use-gnoswap-context"; +import { useWallet } from "@hooks/wallet/use-wallet"; +import { PoolPositionModel } from "@models/position/pool-position-model"; +import { useRouter } from "next/router"; import React, { useCallback } from "react"; -const RemovePositionModalContainer = () => { +interface RemovePositionModalContainerProps { + positions: PoolPositionModel[]; +} + +const RemovePositionModalContainer = ({ + positions, +}: RemovePositionModalContainerProps) => { + const { account } = useWallet(); + const { positionRepository } = useGnoswapContext(); + const router = useRouter(); const clearModal = useClearModal(); const close = useCallback(() => { clearModal(); }, [clearModal]); - const onSubmit = useCallback(() => { - clearModal(); - }, [clearModal]); + const onSubmit = useCallback(async () => { + const address = account?.address; + if (!address) { + return null; + } + const lpTokenIds = positions.map(position => position.id); + const result = await positionRepository.removeLiquidity({ + lpTokenIds, + caller: address + }).catch(() => null); + if (result) { + clearModal(); + router.back(); + } + }, [account?.address, clearModal, positionRepository, positions, router]); - return ; + return ; }; export default RemovePositionModalContainer; diff --git a/packages/web/src/hooks/earn/use-remove-position-modal.tsx b/packages/web/src/hooks/earn/use-remove-position-modal.tsx index 63dfd46ea..853cbdfae 100644 --- a/packages/web/src/hooks/earn/use-remove-position-modal.tsx +++ b/packages/web/src/hooks/earn/use-remove-position-modal.tsx @@ -1,20 +1,26 @@ import RemovePositionModalContainer from "@containers/remove-position-modal-container/RemovePositionModalContainer"; +import { PoolPositionModel } from "@models/position/pool-position-model"; import { CommonState } from "@states/index"; import { useAtom } from "jotai"; -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; export interface Props { - openModal: () => void; + positions: PoolPositionModel[]; + selectedIds: string[]; } -export const useRemovePositionModal = (): Props => { +export const useRemovePositionModal = ({ positions, selectedIds }: Props) => { const [, setOpenedModal] = useAtom(CommonState.openedModal); const [, setModalContent] = useAtom(CommonState.modalContent); + const selectedPositions = useMemo(() => { + return positions.filter(position => selectedIds.includes(position.id)); + }, [positions, selectedIds]); + const openModal = useCallback(() => { setOpenedModal(true); - setModalContent(); - }, [setModalContent, setOpenedModal]); + setModalContent(); + }, [selectedPositions, setModalContent, setOpenedModal]); return { openModal, diff --git a/packages/web/src/hooks/stake/use-remove-data.ts b/packages/web/src/hooks/stake/use-remove-data.ts new file mode 100644 index 000000000..f46f9be34 --- /dev/null +++ b/packages/web/src/hooks/stake/use-remove-data.ts @@ -0,0 +1,98 @@ +import { useTokenData } from "@hooks/token/use-token-data"; +import { PoolPositionModel } from "@models/position/pool-position-model"; +import { numberToUSD } from "@utils/number-utils"; +import { makeDisplayTokenAmount } from "@utils/token-utils"; +import { useMemo } from "react"; + +export interface RemoveDataProps { + positions: PoolPositionModel[]; +} + +export const useRemoveData = ({ positions }: RemoveDataProps) => { + const { tokenPrices } = useTokenData(); + + const pooledTokenInfos = useMemo(() => { + if (positions.length === 0) { + return []; + } + const tokenA = positions[0].pool.tokenA; + const tokenB = positions[0].pool.tokenB; + const pooledTokenAAmount = positions.reduce( + (accum, position) => accum + position.token0Balance, + 0n, + ); + const pooledTokenBAmount = positions.reduce( + (accum, position) => accum + position.token1Balance, + 0n, + ); + const tokenAPrice = tokenPrices[tokenA.priceId]?.usd || 0; + const tokenBPrice = tokenPrices[tokenB.priceId]?.usd || 0; + const tokenAAmount = + makeDisplayTokenAmount(tokenA, Number(pooledTokenAAmount)) || 0; + const tokenBAmount = + makeDisplayTokenAmount(tokenB, Number(pooledTokenBAmount)) || 0; + return [ + { + token: tokenA, + amount: tokenAAmount, + amountUSD: numberToUSD(tokenAAmount * Number(tokenAPrice)), + }, + { + token: tokenB, + amount: tokenBAmount, + amountUSD: numberToUSD(tokenBAmount * Number(tokenBPrice)), + }, + ]; + }, [positions, tokenPrices]); + + const unclaimedRewards = useMemo(() => { + if (positions.length === 0) { + return []; + } + const tokenA = positions[0].pool.tokenA; + const tokenB = positions[0].pool.tokenB; + const pooledTokenAAmount = positions.reduce( + (accum, position) => accum + position.unclaimedFee0Amount, + 0n, + ); + const pooledTokenBAmount = positions.reduce( + (accum, position) => accum + position.unclaimedFee1Amount, + 0n, + ); + const tokenAPrice = tokenPrices[tokenA.priceId]?.usd || 0; + const tokenBPrice = tokenPrices[tokenB.priceId]?.usd || 0; + const tokenAAmount = + makeDisplayTokenAmount(tokenA, Number(pooledTokenAAmount)) || 0; + const tokenBAmount = + makeDisplayTokenAmount(tokenB, Number(pooledTokenBAmount)) || 0; + return [ + { + token: tokenA, + amount: tokenAAmount, + amountUSD: numberToUSD(tokenAAmount * Number(tokenAPrice)), + }, + { + token: tokenB, + amount: tokenBAmount, + amountUSD: numberToUSD(tokenBAmount * Number(tokenBPrice)), + }, + ]; + }, [positions, tokenPrices]); + + const totalLiquidityUSD = useMemo(() => { + if (positions.length === 0) { + return "-"; + } + const totalUSDValue = positions.reduce( + (accum, position) => accum + Number(position.positionUsdValue), + 0, + ); + return numberToUSD(totalUSDValue); + }, [positions]); + + return { + pooledTokenInfos, + unclaimedRewards, + totalLiquidityUSD, + }; +}; diff --git a/packages/web/src/repositories/position/position-repository-impl.ts b/packages/web/src/repositories/position/position-repository-impl.ts index f1208d871..4b898c338 100644 --- a/packages/web/src/repositories/position/position-repository-impl.ts +++ b/packages/web/src/repositories/position/position-repository-impl.ts @@ -8,12 +8,12 @@ import { DEFAULT_TRANSACTION_DEADLINE, } from "@common/values"; import { GnoProvider } from "@gnolang/gno-js-client"; -import { MAX_INT64 } from "@gnoswap-labs/swap-router"; import { PositionMapper } from "@models/position/mapper/position-mapper"; import { PositionModel } from "@models/position/position-model"; +import { MAX_INT256 } from "@utils/math.utils"; import { PositionRepository } from "./position-repository"; import { ClaimAllRequest } from "./request/claim-all-request"; -import { DecreaseLiquidityReqeust } from "./request/decrease-liquidity-request"; +import { RemoveLiquidityReqeust } from "./request/remove-liquidity-request"; import { StakePositionsRequest } from "./request/stake-positions-request"; import { UnstakePositionsRequest } from "./request/unstake-positions-request"; import { PositionListResponse } from "./response"; @@ -124,28 +124,18 @@ export class PositionRepositoryImpl implements PositionRepository { return hash; }; - decreaseLiquidity = async ( - request: DecreaseLiquidityReqeust, + removeLiquidity = async ( + request: RemoveLiquidityReqeust, ): Promise => { if (this.walletClient === null) { throw new CommonError("FAILED_INITIALIZE_WALLET"); } - const { - lpTokenId, - liquidity, - amountAMin, - amountBMax, - caller, - deadline = DEFAULT_TRANSACTION_DEADLINE, - } = request; - const messages = []; - messages.push( + const { lpTokenIds, caller } = request; + const messages = lpTokenIds.map(lpTokenId => PositionRepositoryImpl.makeDecreaseLiquidityMessage( lpTokenId, - liquidity, - amountAMin, - amountBMax, - deadline, + MAX_INT256.toString(), + DEFAULT_TRANSACTION_DEADLINE, caller, ), ); @@ -167,7 +157,12 @@ export class PositionRepositoryImpl implements PositionRepository { send: "", pkg_path: POSITION_PATH, func: "Collect", - args: [lpTokenId, receipient, MAX_INT64.toString(), MAX_INT64.toString()], + args: [ + lpTokenId, + receipient, + MAX_INT256.toString(), + MAX_INT256.toString(), + ], }; } @@ -216,8 +211,6 @@ export class PositionRepositoryImpl implements PositionRepository { private static makeDecreaseLiquidityMessage( lpTokenId: string, liquidity: string, - amountAMin: string, - amountBMin: string, deadeline: string, caller: string, ) { @@ -226,7 +219,7 @@ export class PositionRepositoryImpl implements PositionRepository { send: "", pkg_path: POSITION_PATH, func: "DecreaseLiquidity", - args: [lpTokenId, liquidity, amountAMin, amountBMin, deadeline], + args: [lpTokenId, liquidity, deadeline], }; } @@ -237,8 +230,6 @@ export class PositionRepositoryImpl implements PositionRepository { return this.makeDecreaseLiquidityMessage( lpTokenId, "0", - "0", - "0", DEFAULT_TRANSACTION_DEADLINE, caller, ); diff --git a/packages/web/src/repositories/position/position-repository.ts b/packages/web/src/repositories/position/position-repository.ts index e8bb5e812..57cd27c27 100644 --- a/packages/web/src/repositories/position/position-repository.ts +++ b/packages/web/src/repositories/position/position-repository.ts @@ -1,6 +1,6 @@ import { PositionModel } from "@models/position/position-model"; import { ClaimAllRequest } from "./request/claim-all-request"; -import { DecreaseLiquidityReqeust } from "./request/decrease-liquidity-request"; +import { RemoveLiquidityReqeust } from "./request/remove-liquidity-request"; import { StakePositionsRequest } from "./request/stake-positions-request"; import { UnstakePositionsRequest } from "./request/unstake-positions-request"; @@ -15,7 +15,5 @@ export interface PositionRepository { request: UnstakePositionsRequest, ) => Promise; - decreaseLiquidity: ( - request: DecreaseLiquidityReqeust, - ) => Promise; + removeLiquidity: (request: RemoveLiquidityReqeust) => Promise; } diff --git a/packages/web/src/repositories/position/request/decrease-liquidity-request.ts b/packages/web/src/repositories/position/request/decrease-liquidity-request.ts index 69df49754..a243ca9bf 100644 --- a/packages/web/src/repositories/position/request/decrease-liquidity-request.ts +++ b/packages/web/src/repositories/position/request/decrease-liquidity-request.ts @@ -5,7 +5,7 @@ export interface DecreaseLiquidityReqeust { amountAMin: string; - amountBMax: string; + amountBMin: string; deadline?: string; diff --git a/packages/web/src/repositories/position/request/remove-liquidity-request.ts b/packages/web/src/repositories/position/request/remove-liquidity-request.ts new file mode 100644 index 000000000..c73d077a1 --- /dev/null +++ b/packages/web/src/repositories/position/request/remove-liquidity-request.ts @@ -0,0 +1,5 @@ +export interface RemoveLiquidityReqeust { + lpTokenIds: string[]; + + caller: string; +}