From d048ebcc2d0f7b757c02d250d4e0fba1f07f839c Mon Sep 17 00:00:00 2001 From: jinoosss Date: Wed, 13 Dec 2023 03:42:26 +0900 Subject: [PATCH] feat: [GSW-469] Execute a Reward Claim Contract --- packages/web/.env.example | 1 + .../web/src/common/values/data-constant.ts | 4 + .../MyLiquidityContent.tsx | 4 + .../pool/my-liquidity/MyLiquidity.tsx | 3 + .../MyLiquidityContainer.tsx | 11 +- packages/web/src/hooks/common/use-position.ts | 31 ++++ .../GnoswapServiceProvider.tsx | 4 +- .../position/position-repository-impl.ts | 147 +++++++++++++++++- .../position/position-repository.ts | 8 + .../position/request/claim-all-request.ts | 5 + .../request/decrease-liquidity-request.ts | 13 ++ 11 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 packages/web/src/hooks/common/use-position.ts create mode 100644 packages/web/src/repositories/position/request/claim-all-request.ts create mode 100644 packages/web/src/repositories/position/request/decrease-liquidity-request.ts diff --git a/packages/web/.env.example b/packages/web/.env.example index 87129073b..d5b2d39f5 100644 --- a/packages/web/.env.example +++ b/packages/web/.env.example @@ -6,4 +6,5 @@ NEXT_PUBLIC_PACKAGE_POOL_PATH="gno.land/r/pool" NEXT_PUBLIC_PACKAGE_POOL_ADDRESS="g1ee305k8yk0pjz443xpwtqdyep522f9g5r7d63w" NEXT_PUBLIC_PACKAGE_POSITION_PATH="gno.land/r/position" NEXT_PUBLIC_PACKAGE_POSITION_ADDRESS="g1htpxzv2dkplvzg50nd8fswrneaxmdpwn459thx" +NEXT_PUBLIC_PACKAGE_STAKER_PATH="gno.land/r/staker" NEXT_PUBLIC_WRAPPED_GNOT_PATH="gno.land/r/wugnot" \ No newline at end of file diff --git a/packages/web/src/common/values/data-constant.ts b/packages/web/src/common/values/data-constant.ts index c976f1735..a9d8df946 100644 --- a/packages/web/src/common/values/data-constant.ts +++ b/packages/web/src/common/values/data-constant.ts @@ -23,3 +23,7 @@ export enum NotificationType { "UnWrap" = 10, } export type MathSymbolType = "NEGATIVE" | "POSITIVE" | "NAN"; + +export const DEFAULT_TRANSACTION_DEADLINE = "7282571140" as const; +export const DEFAULT_GAS_FEE = 1 as const; +export const DEFAULT_GAS_WANTED = 2000000 as const; diff --git a/packages/web/src/components/pool/my-liquidity-content/MyLiquidityContent.tsx b/packages/web/src/components/pool/my-liquidity-content/MyLiquidityContent.tsx index 57589d250..842c29f96 100644 --- a/packages/web/src/components/pool/my-liquidity-content/MyLiquidityContent.tsx +++ b/packages/web/src/components/pool/my-liquidity-content/MyLiquidityContent.tsx @@ -23,12 +23,14 @@ interface MyLiquidityContentProps { positions: PoolPositionModel[]; breakpoint: DEVICE_TYPE; isDisabledButton: boolean; + claimAll: () => void; } const MyLiquidityContent: React.FC = ({ connected, positions, breakpoint, + claimAll, }) => { const { tokenPrices } = useTokenData(); @@ -216,6 +218,7 @@ const MyLiquidityContent: React.FC = ({ padding: "10px 16px", fontType: "p1", }} + onClick={claimAll} /> ) : ( @@ -245,6 +248,7 @@ const MyLiquidityContent: React.FC = ({ padding: "0px 16px", fontType: "p1", }} + onClick={claimAll} /> diff --git a/packages/web/src/components/pool/my-liquidity/MyLiquidity.tsx b/packages/web/src/components/pool/my-liquidity/MyLiquidity.tsx index eb72b9954..047e204bd 100644 --- a/packages/web/src/components/pool/my-liquidity/MyLiquidity.tsx +++ b/packages/web/src/components/pool/my-liquidity/MyLiquidity.tsx @@ -16,6 +16,7 @@ interface MyLiquidityProps { divRef: React.RefObject; onScroll: () => void; currentIndex: number; + claimAll: () => void; } const MyLiquidity: React.FC = ({ @@ -28,6 +29,7 @@ const MyLiquidity: React.FC = ({ divRef, onScroll, currentIndex, + claimAll, }) => { return ( @@ -43,6 +45,7 @@ const MyLiquidity: React.FC = ({ positions={positions} breakpoint={breakpoint} isDisabledButton={isSwitchNetwork || !connected} + claimAll={claimAll} /> diff --git a/packages/web/src/containers/my-liquidity-container/MyLiquidityContainer.tsx b/packages/web/src/containers/my-liquidity-container/MyLiquidityContainer.tsx index 04048fd8e..affd0e2bb 100644 --- a/packages/web/src/containers/my-liquidity-container/MyLiquidityContainer.tsx +++ b/packages/web/src/containers/my-liquidity-container/MyLiquidityContainer.tsx @@ -5,17 +5,19 @@ import { useWallet } from "@hooks/wallet/use-wallet"; import { useRouter } from "next/router"; import { usePositionData } from "@hooks/common/use-position-data"; import { PoolPositionModel } from "@models/position/pool-position-model"; +import { usePosition } from "@hooks/common/use-position"; const MyLiquidityContainer: React.FC = () => { + const router = useRouter(); + const divRef = useRef(null); const { breakpoint } = useWindowSize(); const { connected: connectedWallet, isSwitchNetwork, account } = useWallet(); const { getPositionsByPoolId } = usePositionData(); - const router = useRouter(); - const divRef = useRef(null); const [currentIndex, setCurrentIndex] = useState(1); const [positions, setPositions] = useState([]); + const { claimAll } = usePosition(positions); useEffect(() => { const poolPath = router.query["pool-path"] as string; @@ -42,6 +44,10 @@ const MyLiquidityContainer: React.FC = () => { } }; + const claimAllReward = useCallback(() => { + claimAll(); + }, [claimAll]); + return ( { divRef={divRef} onScroll={handleScroll} currentIndex={currentIndex} + claimAll={claimAllReward} /> ); }; diff --git a/packages/web/src/hooks/common/use-position.ts b/packages/web/src/hooks/common/use-position.ts new file mode 100644 index 000000000..20ab7b9f9 --- /dev/null +++ b/packages/web/src/hooks/common/use-position.ts @@ -0,0 +1,31 @@ +import { useWallet } from "@hooks/wallet/use-wallet"; +import { PoolPositionModel } from "@models/position/pool-position-model"; +import { useCallback } from "react"; +import { useGnoswapContext } from "./use-gnoswap-context"; + +export const usePosition = (positions: PoolPositionModel[]) => { + const { positionRepository } = useGnoswapContext(); + const { account } = useWallet(); + + const claimAll = useCallback(() => { + const address = account?.address; + if (!address) { + return null; + } + + const lpTokenIds = positions + .filter( + position => + position.unclaimedFee0Amount + position.unclaimedFee1Amount > 0, + ) + .map(position => position.lpTokenId); + return positionRepository.claimAll({ + lpTokenIds, + receipient: address, + }); + }, [account?.address, positionRepository, positions]); + + return { + claimAll, + }; +}; diff --git a/packages/web/src/providers/gnoswap-service-provider/GnoswapServiceProvider.tsx b/packages/web/src/providers/gnoswap-service-provider/GnoswapServiceProvider.tsx index c1b41e264..208fb7a23 100644 --- a/packages/web/src/providers/gnoswap-service-provider/GnoswapServiceProvider.tsx +++ b/packages/web/src/providers/gnoswap-service-provider/GnoswapServiceProvider.tsx @@ -96,8 +96,8 @@ const GnoswapServiceProvider: React.FC = ({ }, [localStorageClient, networkClient]); const positionRepository = useMemo(() => { - return new PositionRepositoryImpl(networkClient); - }, [networkClient]); + return new PositionRepositoryImpl(networkClient, rpcProvider, walletClient); + }, [networkClient, rpcProvider, walletClient]); async function initNetwork() { const defaultChainId = process.env.NEXT_PUBLIC_DEFAULT_CHAIN_ID || DEFAULT_NETWORK_ID; diff --git a/packages/web/src/repositories/position/position-repository-impl.ts b/packages/web/src/repositories/position/position-repository-impl.ts index 913790525..1aa9dff59 100644 --- a/packages/web/src/repositories/position/position-repository-impl.ts +++ b/packages/web/src/repositories/position/position-repository-impl.ts @@ -1,14 +1,37 @@ import { NetworkClient } from "@common/clients/network-client"; +import { WalletClient } from "@common/clients/wallet-client"; +import { SendTransactionSuccessResponse } from "@common/clients/wallet-client/protocols"; +import { CommonError } from "@common/errors"; +import { + DEFAULT_GAS_FEE, + DEFAULT_GAS_WANTED, + 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 { PositionRepository } from "./position-repository"; +import { ClaimAllRequest } from "./request/claim-all-request"; +import { DecreaseLiquidityReqeust } from "./request/decrease-liquidity-request"; import { PositionListResponse } from "./response"; +const STAKER_PATH = process.env.NEXT_PUBLIC_PACKAGE_STAKER_PATH || ""; +const POSITION_PATH = process.env.NEXT_PUBLIC_PACKAGE_POSITION_PATH || ""; + export class PositionRepositoryImpl implements PositionRepository { private networkClient: NetworkClient; + private rpcProvider: GnoProvider | null; + private walletClient: WalletClient | null; - constructor(networkClient: NetworkClient) { + constructor( + networkClient: NetworkClient, + rpcProvider: GnoProvider | null, + walletClient: WalletClient | null, + ) { this.networkClient = networkClient; + this.rpcProvider = rpcProvider; + this.walletClient = walletClient; } getPositionsByAddress = async (address: string): Promise => { @@ -17,4 +40,126 @@ export class PositionRepositoryImpl implements PositionRepository { }); return PositionMapper.fromList(response.data); }; + + claimAll = async (request: ClaimAllRequest): Promise => { + if (this.walletClient === null) { + throw new CommonError("FAILED_INITIALIZE_WALLET"); + } + const { lpTokenIds, receipient } = request; + const messages = lpTokenIds.flatMap(lpTokenId => { + const messages = []; + messages.push( + PositionRepositoryImpl.makeZeroDecreaseLiquidityMessage( + lpTokenId, + receipient, + ), + ); + messages.push( + PositionRepositoryImpl.makeCollectMessage(lpTokenId, receipient), + ); + return messages; + }); + + // TODO: Need to check if a contract error occurred + // messages.push(PositionRepositoryImpl.makeCollectRewardMessage(receipient)); + + const result = await this.walletClient.sendTransaction({ + messages, + gasFee: DEFAULT_GAS_FEE, + gasWanted: DEFAULT_GAS_WANTED, + }); + const hash = (result.data as SendTransactionSuccessResponse)?.hash || null; + if (!hash) { + throw new Error(`${result}`); + } + return hash; + }; + + decreaseLiquidity = async ( + request: DecreaseLiquidityReqeust, + ): 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( + PositionRepositoryImpl.makeDecreaseLiquidityMessage( + lpTokenId, + liquidity, + amountAMin, + amountBMax, + deadline, + caller, + ), + ); + const result = await this.walletClient.sendTransaction({ + messages, + gasFee: DEFAULT_GAS_FEE, + gasWanted: DEFAULT_GAS_WANTED, + }); + const hash = (result.data as SendTransactionSuccessResponse)?.hash || null; + if (!hash) { + throw new Error(`${result}`); + } + return hash; + }; + + private static makeCollectMessage(lpTokenId: string, receipient: string) { + return { + caller: receipient, + send: "", + pkg_path: POSITION_PATH, + func: "Collect", + args: [lpTokenId, receipient, MAX_INT64.toString(), MAX_INT64.toString()], + }; + } + + private static makeCollectRewardMessage(caller: string) { + return { + caller, + send: "", + pkg_path: STAKER_PATH, + func: "CollectReward", + args: [], + }; + } + + private static makeDecreaseLiquidityMessage( + lpTokenId: string, + liquidity: string, + amountAMin: string, + amountBMin: string, + deadeline: string, + caller: string, + ) { + return { + caller, + send: "", + pkg_path: POSITION_PATH, + func: "DecreaseLiquidity", + args: [lpTokenId, liquidity, amountAMin, amountBMin, deadeline], + }; + } + + private static makeZeroDecreaseLiquidityMessage( + lpTokenId: string, + caller: string, + ) { + 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 00ef28c1a..152599900 100644 --- a/packages/web/src/repositories/position/position-repository.ts +++ b/packages/web/src/repositories/position/position-repository.ts @@ -1,5 +1,13 @@ import { PositionModel } from "@models/position/position-model"; +import { ClaimAllRequest } from "./request/claim-all-request"; +import { DecreaseLiquidityReqeust } from "./request/decrease-liquidity-request"; export interface PositionRepository { getPositionsByAddress: (address: string) => Promise; + + claimAll: (request: ClaimAllRequest) => Promise; + + decreaseLiquidity: ( + request: DecreaseLiquidityReqeust, + ) => Promise; } diff --git a/packages/web/src/repositories/position/request/claim-all-request.ts b/packages/web/src/repositories/position/request/claim-all-request.ts new file mode 100644 index 000000000..70a831457 --- /dev/null +++ b/packages/web/src/repositories/position/request/claim-all-request.ts @@ -0,0 +1,5 @@ +export interface ClaimAllRequest { + lpTokenIds: string[]; + + receipient: string; +} diff --git a/packages/web/src/repositories/position/request/decrease-liquidity-request.ts b/packages/web/src/repositories/position/request/decrease-liquidity-request.ts new file mode 100644 index 000000000..69df49754 --- /dev/null +++ b/packages/web/src/repositories/position/request/decrease-liquidity-request.ts @@ -0,0 +1,13 @@ +export interface DecreaseLiquidityReqeust { + lpTokenId: string; + + liquidity: string; + + amountAMin: string; + + amountBMax: string; + + deadline?: string; + + caller: string; +}