From 4bf724d250e2730d8f52a473e0513275e1745a64 Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Fri, 6 Dec 2024 19:26:36 +0530 Subject: [PATCH 01/10] feat(web): new-stake-flow-v1 --- web/package.json | 4 +- web/src/assets/svgs/icons/close-circle.svg | 2 +- web/src/components/ExternalLink.tsx | 3 +- web/src/hooks/queries/useCourtDetails.ts | 1 + .../CourtDetails/StakePanel/InputDisplay.tsx | 26 +- .../SimulatorPopup/QuantityToSimulate.tsx | 6 +- .../StakePanel/StakeWithdrawButton.tsx | 273 ++++++++++++----- .../StakePanel/StakeWithdrawPopup/Header.tsx | 91 ++++++ .../StakePanel/StakeWithdrawPopup/index.tsx | 99 +++++++ .../StakeWithdrawPopup/stakeSteps.tsx | 276 ++++++++++++++++++ .../Courts/CourtDetails/StakePanel/index.tsx | 31 +- web/src/pages/Courts/CourtDetails/index.tsx | 5 +- web/src/utils/parseWagmiError.ts | 7 +- web/tsconfig.json | 1 + yarn.lock | 241 ++++++++++++++- 15 files changed, 937 insertions(+), 129 deletions(-) create mode 100644 web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawPopup/Header.tsx create mode 100644 web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawPopup/index.tsx create mode 100644 web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawPopup/stakeSteps.tsx diff --git a/web/package.json b/web/package.json index c12dbc666..097597919 100644 --- a/web/package.json +++ b/web/package.json @@ -117,7 +117,7 @@ "react-toastify": "^9.1.3", "react-use": "^17.5.1", "styled-components": "^5.3.3", - "viem": "^2.21.48", - "wagmi": "^2.13.0" + "viem": "^2.21.54", + "wagmi": "^2.13.3" } } diff --git a/web/src/assets/svgs/icons/close-circle.svg b/web/src/assets/svgs/icons/close-circle.svg index f3d4a2477..1f4c4efd6 100644 --- a/web/src/assets/svgs/icons/close-circle.svg +++ b/web/src/assets/svgs/icons/close-circle.svg @@ -1,3 +1,3 @@ - + diff --git a/web/src/components/ExternalLink.tsx b/web/src/components/ExternalLink.tsx index f85920a1a..8470386ec 100644 --- a/web/src/components/ExternalLink.tsx +++ b/web/src/components/ExternalLink.tsx @@ -1,6 +1,7 @@ -import { Link } from "react-router-dom"; import styled from "styled-components"; +import { Link } from "react-router-dom"; + export const ExternalLink = styled(Link)` :hover { text-decoration: underline; diff --git a/web/src/hooks/queries/useCourtDetails.ts b/web/src/hooks/queries/useCourtDetails.ts index 6d355ac26..296d4aa09 100644 --- a/web/src/hooks/queries/useCourtDetails.ts +++ b/web/src/hooks/queries/useCourtDetails.ts @@ -23,6 +23,7 @@ const courtDetailsQuery = graphql(` paidPNK timesPerPeriod feeForJuror + name } } `); diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx index 8ff616816..5f63b535b 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx @@ -1,25 +1,28 @@ import React, { useState, useMemo, useEffect } from "react"; import styled, { css } from "styled-components"; -import { landscapeStyle } from "styles/landscapeStyle"; import { useParams } from "react-router-dom"; import { useDebounce } from "react-use"; import { useAccount } from "wagmi"; import { REFETCH_INTERVAL } from "consts/index"; - import { useReadSortitionModuleGetJurorBalance, useReadPnkBalanceOf } from "hooks/contracts/generated"; import { useParsedAmount } from "hooks/useParsedAmount"; - import { commify, uncommify } from "utils/commify"; import { formatPNK, roundNumberDown } from "utils/format"; import { isUndefined } from "utils/index"; +import { landscapeStyle } from "styles/landscapeStyle"; + import { NumberInputField } from "components/NumberInputField"; + import StakeWithdrawButton, { ActionType } from "./StakeWithdrawButton"; const StyledField = styled(NumberInputField)` height: fit-content; + input { + border-radius: 3px 0px 0px 3px; + } `; const LabelArea = styled.div` @@ -62,26 +65,17 @@ const EnsureChainContainer = styled.div` button { height: 45px; border: 1px solid ${({ theme }) => theme.stroke}; + border-radius: 0px 3px 3px 0px; } `; interface IInputDisplay { action: ActionType; - isSending: boolean; - setIsSending: (arg0: boolean) => void; - setIsPopupOpen: (arg0: boolean) => void; amount: string; setAmount: (arg0: string) => void; } -const InputDisplay: React.FC = ({ - action, - isSending, - setIsSending, - setIsPopupOpen, - amount, - setAmount, -}) => { +const InputDisplay: React.FC = ({ action, amount, setAmount }) => { const [debouncedAmount, setDebouncedAmount] = useState(""); const [errorMsg, setErrorMsg] = useState(); useDebounce(() => setDebouncedAmount(amount), 500, [amount]); @@ -147,12 +141,10 @@ const InputDisplay: React.FC = ({ diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/SimulatorPopup/QuantityToSimulate.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/SimulatorPopup/QuantityToSimulate.tsx index 4b43352f8..d18b96ddf 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/SimulatorPopup/QuantityToSimulate.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/SimulatorPopup/QuantityToSimulate.tsx @@ -1,5 +1,6 @@ import React from "react"; import styled from "styled-components"; + import Skeleton from "react-loading-skeleton"; import { commify } from "utils/commify"; @@ -13,6 +14,7 @@ const Container = styled.div` align-items: center; flex-wrap: wrap; gap: 0 8px; + justify-content: center; `; const TextWithTooltipContainer = styled.div` @@ -48,6 +50,7 @@ interface IQuantityToSimulate { jurorCurrentSpecificStake: number | undefined; isStaking: boolean; amountToStake: number; + className?: string; } const QuantityToSimulate: React.FC = ({ @@ -55,6 +58,7 @@ const QuantityToSimulate: React.FC = ({ jurorCurrentEffectiveStake, jurorCurrentSpecificStake, amountToStake, + className, }) => { const effectiveStakeDisplay = !isUndefined(jurorCurrentEffectiveStake) ? ( `${commify(jurorCurrentEffectiveStake)} PNK` @@ -85,7 +89,7 @@ const QuantityToSimulate: React.FC = ({ ); return ( - + {effectiveStakeDisplay} void; setAmount: (arg0: string) => void; - setIsPopupOpen: (arg0: boolean) => void; setErrorMsg: (msg: string) => void; } -const StakeWithdrawButton: React.FC = ({ - parsedAmount, - action, - isSending, - setIsSending, - setIsPopupOpen, - setErrorMsg, -}) => { +const StakeWithdrawButton: React.FC = ({ amount, parsedAmount, action, setErrorMsg, setAmount }) => { const { id } = useParams(); const { address } = useAccount(); + const theme = useTheme(); + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [popupStepsState, setPopupStepsState] = useState<{ + items: [_TimelineItem1, ..._TimelineItem1[]]; + current: number; + }>(); + const { data: courtDetails } = useCourtDetails(id); const { data: balance } = useReadPnkBalanceOf({ query: { @@ -65,6 +68,7 @@ const StakeWithdrawButton: React.FC = ({ }, args: [address!], }); + const { data: jurorBalance } = useReadSortitionModuleGetJurorBalance({ query: { enabled: !isUndefined(address), @@ -72,7 +76,7 @@ const StakeWithdrawButton: React.FC = ({ }, args: [address ?? "0x", BigInt(id ?? 0)], }); - const { data: allowance } = useReadPnkAllowance({ + const { data: allowance, refetch: refetchAllowance } = useReadPnkAllowance({ query: { enabled: !isUndefined(address), refetchInterval: REFETCH_INTERVAL, @@ -97,86 +101,215 @@ const StakeWithdrawButton: React.FC = ({ return 0n; }, [jurorBalance, parsedAmount, isAllowance, isStaking]); - const { data: increaseAllowanceConfig } = useSimulatePnkIncreaseAllowance({ + const { + data: increaseAllowanceConfig, + isLoading: isSimulatingAllowance, + error: allowanceError, + } = useSimulatePnkIncreaseAllowance({ query: { - enabled: isAllowance && !isUndefined(targetStake) && !isUndefined(allowance), + enabled: + isAllowance && !isUndefined(targetStake) && !isUndefined(allowance) && !isUndefined(balance) && !isPopupOpen, }, args: [klerosCoreAddress[DEFAULT_CHAIN], BigInt(targetStake ?? 0) - BigInt(allowance ?? 0)], }); const { writeContractAsync: increaseAllowance } = useWritePnkIncreaseAllowance(); - const handleAllowance = useCallback(() => { - if (increaseAllowanceConfig && publicClient) { - setIsSending(true); - wrapWithToast(async () => await increaseAllowance(increaseAllowanceConfig.request), publicClient).finally(() => { - setIsSending(false); - }); - } - }, [setIsSending, increaseAllowance, increaseAllowanceConfig, publicClient]); - - const { data: setStakeConfig, error: setStakeError } = useSimulateKlerosCoreSetStake({ + const { + data: setStakeConfig, + error: setStakeError, + isLoading: isSimulatingSetStake, + refetch: refetchSetStake, + } = useSimulateKlerosCoreSetStake({ query: { enabled: - !isUndefined(targetStake) && !isUndefined(id) && !isAllowance && parsedAmount !== 0n && targetStake >= 0n, + !isUndefined(targetStake) && + !isUndefined(id) && + parsedAmount !== 0n && + targetStake >= 0n && + !isAllowance && + (isStaking ? true : jurorBalance && parsedAmount <= jurorBalance[2]), }, args: [BigInt(id ?? 0), targetStake], }); const { writeContractAsync: setStake } = useWriteKlerosCoreSetStake(); - const handleStake = useCallback(() => { - if (setStakeConfig && publicClient) { - setIsSending(true); - wrapWithToast(async () => await setStake(setStakeConfig.request), publicClient) - .then((res) => setIsPopupOpen(res.status)) - .finally(() => { - setIsSending(false); + const handleStake = useCallback( + (config?: typeof setStakeConfig, approvalHash?: `0x${string}`) => { + const isWithdraw = action === ActionType.withdraw; + const requestData = config?.request ?? setStakeConfig?.request; + + if (requestData && publicClient) { + setPopupStepsState({ + items: getStakeSteps( + isWithdraw ? StakeSteps.WithdrawInitiate : StakeSteps.StakeInitiate, + amount, + theme, + approvalHash, + undefined + ), + current: 1, }); - } - }, [setIsSending, setStake, setStakeConfig, publicClient, setIsPopupOpen]); - const buttonProps = { - [ActionType.allowance]: { - text: "Allow PNK", - checkDisabled: () => !balance || targetStake > balance, - onClick: handleAllowance, - }, - [ActionType.stake]: { - text: "Stake", - checkDisabled: () => !isUndefined(setStakeError), - onClick: handleStake, - }, - [ActionType.withdraw]: { - text: "Withdraw", - checkDisabled: () => !jurorBalance || parsedAmount > jurorBalance[2], - onClick: handleStake, + setStake(requestData) + .then(async (hash) => { + setPopupStepsState({ + items: getStakeSteps( + isWithdraw ? StakeSteps.WithdrawPending : StakeSteps.StakePending, + amount, + theme, + approvalHash, + hash + ), + current: 1, + }); + await publicClient.waitForTransactionReceipt({ hash, confirmations: 2 }).then((res: TransactionReceipt) => { + const status = res.status === "success"; + if (status) { + setPopupStepsState({ + items: getStakeSteps( + isWithdraw ? StakeSteps.WithdrawConfirmed : StakeSteps.StakeConfirmed, + amount, + theme, + approvalHash, + hash + ), + current: 1, + }); + setIsSuccess(true); + } else + setPopupStepsState({ + items: getStakeSteps( + isWithdraw ? StakeSteps.WithdrawFailed : StakeSteps.StakeFailed, + amount, + theme, + approvalHash, + hash + ), + current: 1, + }); + }); + }) + .catch((err) => { + setPopupStepsState({ + items: getStakeSteps( + isWithdraw ? StakeSteps.WithdrawFailed : StakeSteps.StakeFailed, + amount, + theme, + approvalHash, + undefined, + err + ), + current: 1, + }); + }); + } }, - }; + [setStake, setStakeConfig, publicClient, amount, theme, action] + ); + + const handleClick = useCallback(() => { + setIsPopupOpen(true); + if (ActionType.allowance && isAllowance && increaseAllowanceConfig && publicClient) { + setPopupStepsState({ + items: getStakeSteps(StakeSteps.ApproveInitiate, amount, theme), + current: 0, + }); + + increaseAllowance(increaseAllowanceConfig.request) + .then(async (hash) => { + setPopupStepsState({ + items: getStakeSteps(StakeSteps.ApprovePending, amount, theme, hash), + current: 0, + }); + + await publicClient + .waitForTransactionReceipt({ hash, confirmations: 2 }) + .then(async (res: TransactionReceipt) => { + const status = res.status === "success"; + if (status) { + await refetchAllowance(); + const refetchData = await refetchSetStake(); + + handleStake(refetchData.data, hash); + } else + setPopupStepsState({ + items: getStakeSteps(StakeSteps.ApproveFailed, amount, theme, hash), + current: 0, + }); + }); + }) + .catch((err) => { + setPopupStepsState({ + items: getStakeSteps(StakeSteps.ApproveFailed, amount, theme, undefined, undefined, err), + current: 0, + }); + }); + } else { + handleStake(); + } + }, [ + increaseAllowance, + increaseAllowanceConfig, + handleStake, + isAllowance, + theme, + publicClient, + amount, + refetchAllowance, + refetchSetStake, + ]); useEffect(() => { - if (setStakeError) { - setErrorMsg(parseWagmiError(setStakeError)); + if (setStakeError || allowanceError) { + setErrorMsg(parseWagmiError(setStakeError || allowanceError)); + } else if (targetStake !== 0n && courtDetails && targetStake < BigInt(courtDetails.court?.minStake)) { + setErrorMsg(`Min Stake in court is: ${formatETH(courtDetails?.court?.minStake)}`); + } + }, [setStakeError, setErrorMsg, targetStake, courtDetails, allowanceError]); + + const isDisabled = useMemo(() => { + if ( + parsedAmount == 0n || + isUndefined(targetStake) || + isUndefined(courtDetails) || + (targetStake !== 0n && targetStake < BigInt(courtDetails.court?.minStake)) + ) + return true; + if (isAllowance) { + return isUndefined(increaseAllowanceConfig) || isSimulatingAllowance || !isUndefined(allowanceError); } - }, [setStakeError, setErrorMsg]); - const { text, checkDisabled, onClick } = buttonProps[isAllowance ? ActionType.allowance : action]; + return isUndefined(setStakeConfig) || isSimulatingSetStake || !isUndefined(setStakeError); + }, [ + parsedAmount, + targetStake, + courtDetails, + increaseAllowanceConfig, + isSimulatingAllowance, + setStakeConfig, + isSimulatingSetStake, + setStakeError, + allowanceError, + isAllowance, + ]); + + const closePopup = () => { + setIsPopupOpen(false); + setIsSuccess(false); + setAmount(""); + }; + return (