From 4300cc4091df3242deaa89978d7e8bbf75b10310 Mon Sep 17 00:00:00 2001 From: David Totraev Date: Wed, 4 Dec 2024 14:51:33 +0500 Subject: [PATCH] feat: hook up modals --- package-lock.json | 16 +- package.json | 2 +- .../components/Delegations/Delegations.tsx | 16 +- .../components/Modals/ConfirmationModal.tsx | 64 ++++++++ .../Modals/PendingVerificationModal.tsx | 13 +- src/app/components/Modals/PreviewModal.tsx | 10 +- .../components/Modals/ResponsiveDialog.tsx | 10 ++ src/app/components/Modals/SlashingModal.tsx | 22 +++ src/app/components/Modals/StakeModal.tsx | 32 ++++ src/app/components/Modals/SubmitModal.tsx | 80 ++++++++++ .../Modals/TransitionModal/StageStepping.tsx | 35 +---- .../Modals/TransitionModal/Step.tsx | 36 +++++ .../components/Modals/UnbondErrorModal.tsx | 13 +- src/app/components/Modals/UnbondModal.tsx | 90 ++++-------- src/app/components/Modals/WithdrawModal.tsx | 74 ++-------- src/app/components/Staking/Staking.tsx | 64 ++++---- .../hooks/services/useDelegationService.ts | 137 ++++++++++++------ src/app/hooks/storage/useDelegationStorage.ts | 11 +- src/app/types/delegationsV2.ts | 1 + .../components/ActionButton.tsx | 17 ++- .../components/DelegationModal.tsx | 66 +++++++++ .../DelegationList/components/Inception.tsx | 1 + .../delegations/DelegationList/index.tsx | 39 +++-- 23 files changed, 557 insertions(+), 292 deletions(-) create mode 100644 src/app/components/Modals/ConfirmationModal.tsx create mode 100644 src/app/components/Modals/ResponsiveDialog.tsx create mode 100644 src/app/components/Modals/SlashingModal.tsx create mode 100644 src/app/components/Modals/StakeModal.tsx create mode 100644 src/app/components/Modals/SubmitModal.tsx create mode 100644 src/app/components/Modals/TransitionModal/Step.tsx create mode 100644 src/components/delegations/DelegationList/components/DelegationModal.tsx diff --git a/package-lock.json b/package-lock.json index c1340058..f5525539 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@sentry/nextjs": "^8.30.0", "@tanstack/react-query": "^5.28.14", "@tanstack/react-query-next-experimental": "^5.28.14", - "@tomo-inc/wallet-connect-sdk": "^0.3.3", + "@tomo-inc/wallet-connect-sdk": "^0.3.5", "@uidotdev/usehooks": "^2.4.1", "axios": "^1.7.4", "bitcoinjs-lib": "6.1.5", @@ -6629,9 +6629,9 @@ } }, "node_modules/@tomo-inc/tomo-wallet-provider": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@tomo-inc/tomo-wallet-provider/-/tomo-wallet-provider-1.0.25.tgz", - "integrity": "sha512-P84NzQ6xiRuiQgVJqzfTWjpnlF65NbqYY/L5OaJZbpPSXaFAeNYClIUn40Okxkg/xKwryoOWEr4KzZxiOOVpxg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tomo-inc/tomo-wallet-provider/-/tomo-wallet-provider-1.1.0.tgz", + "integrity": "sha512-aOVGRz2TzmcaccHbCG62iXJMNNmnbOgyPNoSGYXlhT1elvqUTLOGg34SmcgavcXTbPMilwJ9VIew24GF0DAQ0g==", "dependencies": { "events": "^3.3.0", "long": "^5.2.3" @@ -6650,11 +6650,11 @@ } }, "node_modules/@tomo-inc/wallet-connect-sdk": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@tomo-inc/wallet-connect-sdk/-/wallet-connect-sdk-0.3.3.tgz", - "integrity": "sha512-dtfmwL3Rko73WqwbY8cyP793m4Lq/zssPEy0OOSuVl2y6ABvoQRw2Zh+9DhET6U9wt7NP/OYuYYm7F3rwST1fg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@tomo-inc/wallet-connect-sdk/-/wallet-connect-sdk-0.3.5.tgz", + "integrity": "sha512-HDt9H4bsp8hf9KM7ehW3YGffP+AZvMzxz5cqiSixTnGNWIrhDe9gpyVsAshZIJ1IDRfQxstnOMz4NnfQROyonA==", "dependencies": { - "@tomo-inc/tomo-wallet-provider": "^1.0.25", + "@tomo-inc/tomo-wallet-provider": "1.1.0", "animate.css": "^4.1.1", "buffer": "^6.0.3", "classnames": "^2.5.1", diff --git a/package.json b/package.json index 1e00a8dd..98abb274 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@sentry/nextjs": "^8.30.0", "@tanstack/react-query": "^5.28.14", "@tanstack/react-query-next-experimental": "^5.28.14", - "@tomo-inc/wallet-connect-sdk": "^0.3.3", + "@tomo-inc/wallet-connect-sdk": "^0.3.5", "@uidotdev/usehooks": "^2.4.1", "axios": "^1.7.4", "bitcoinjs-lib": "6.1.5", diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index a0528ecf..90849097 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -5,11 +5,7 @@ import InfiniteScroll from "react-infinite-scroll-component"; import { useLocalStorage } from "usehooks-ts"; import { LoadingTableList } from "@/app/components/Loading/Loading"; -import { - MODE, - MODE_WITHDRAW, - WithdrawModal, -} from "@/app/components/Modals/WithdrawModal"; +import { WithdrawModal } from "@/app/components/Modals/WithdrawModal"; import { DelegationsPointsProvider } from "@/app/context/api/DelegationsPointsProvider"; import { useError } from "@/app/context/Error/ErrorContext"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; @@ -28,6 +24,10 @@ import { toLocalStorageIntermediateDelegation } from "@/utils/local_storage/toLo import { Delegation } from "./Delegation"; +const MODE_TRANSITION = "transition"; +const MODE_WITHDRAW = "withdraw"; +type MODE = typeof MODE_TRANSITION | typeof MODE_WITHDRAW; + export const Delegations = () => { const { data: delegationsAPI } = useDelegations(); const { address, publicKeyNoCoord, connected, network } = useBTCWallet(); @@ -298,12 +298,12 @@ const DelegationsContent: React.FC = ({ {modalMode && txID && delegation && ( setModalOpen(false)} - onProceed={() => { + onSubmit={() => { handleWithdraw(txID); }} - awaitingWalletResponse={awaitingWalletResponse} + processing={awaitingWalletResponse} /> )} diff --git a/src/app/components/Modals/ConfirmationModal.tsx b/src/app/components/Modals/ConfirmationModal.tsx new file mode 100644 index 00000000..eb495c16 --- /dev/null +++ b/src/app/components/Modals/ConfirmationModal.tsx @@ -0,0 +1,64 @@ +import { + Button, + DialogBody, + DialogFooter, + DialogHeader, + Loader, +} from "@babylonlabs-io/bbn-core-ui"; +import { PropsWithChildren } from "react"; +import { twMerge } from "tailwind-merge"; + +import { ResponsiveDialog } from "./ResponsiveDialog"; + +interface ConfirmationModalProps { + className?: string; + processing: boolean; + open: boolean; + title: string; + onClose: () => void; + onSubmit: () => void; +} + +export const ConfirmationModal = ({ + className, + processing, + open, + title, + children, + onClose, + onSubmit, +}: PropsWithChildren) => ( + + + + {children} + + + + + + + +); diff --git a/src/app/components/Modals/PendingVerificationModal.tsx b/src/app/components/Modals/PendingVerificationModal.tsx index a4d5701c..6604d3d0 100644 --- a/src/app/components/Modals/PendingVerificationModal.tsx +++ b/src/app/components/Modals/PendingVerificationModal.tsx @@ -4,7 +4,8 @@ import { BiSolidBadgeCheck } from "react-icons/bi"; import { useDelegationV2 } from "@/app/hooks/client/api/useDelegationV2"; import { useTransactionService } from "@/app/hooks/services/useTransactionService"; -import { DelegationV2StakingState as state } from "@/app/types/delegationsV2"; +import { useDelegationV2State } from "@/app/state/DelegationV2State"; +import { DelegationV2StakingState as State } from "@/app/types/delegationsV2"; import { getNetworkConfig } from "@/config/network.config"; import { GeneralModal } from "./GeneralModal"; @@ -45,6 +46,7 @@ export function PendingVerificationModal({ stakingTxHash, }: PendingVerificationModalProps) { const { submitStakingTx } = useTransactionService(); + const { updateDelegationStatus } = useDelegationV2State(); const { networkName } = getNetworkConfig(); const { data: delegation = null } = useDelegationV2(stakingTxHash); @@ -72,10 +74,15 @@ export function PendingVerificationModal({ stakingTxHashHex, stakingTxHex, ); + + updateDelegationStatus( + stakingTxHashHex, + State.INTERMEDIATE_PENDING_BTC_CONFIRMATION, + ); onClose(); - }, [delegation, submitStakingTx, onClose]); + }, [delegation, updateDelegationStatus, submitStakingTx, onClose]); - const verified = delegation?.state === state.VERIFIED; + const verified = delegation?.state === State.VERIFIED; return ( void; @@ -53,8 +53,6 @@ export const PreviewModal = ({ networkInfo?.params.btcEpochCheckParams?.latestParam ?.btcConfirmationDepth || 10; - const DialogComponent = isMobileView ? MobileDialog : Dialog; - const FinalityProviderValue = isMobileView ? ( {finalityProviderAvatar && ( @@ -117,7 +115,7 @@ export const PreviewModal = ({ ]; return ( - + - + ); }; diff --git a/src/app/components/Modals/ResponsiveDialog.tsx b/src/app/components/Modals/ResponsiveDialog.tsx new file mode 100644 index 00000000..e8b612da --- /dev/null +++ b/src/app/components/Modals/ResponsiveDialog.tsx @@ -0,0 +1,10 @@ +import { Dialog, DialogProps, MobileDialog } from "@babylonlabs-io/bbn-core-ui"; + +import { useIsMobileView } from "@/app/hooks/useBreakpoint"; + +export function ResponsiveDialog(props: DialogProps) { + const isMobileView = useIsMobileView(); + const DialogComponent = isMobileView ? MobileDialog : Dialog; + + return ; +} diff --git a/src/app/components/Modals/SlashingModal.tsx b/src/app/components/Modals/SlashingModal.tsx new file mode 100644 index 00000000..e27c55e6 --- /dev/null +++ b/src/app/components/Modals/SlashingModal.tsx @@ -0,0 +1,22 @@ +import { Text } from "@babylonlabs-io/bbn-core-ui"; + +import { ConfirmationModal } from "./ConfirmationModal"; + +interface UnbondModalProps { + processing: boolean; + open: boolean; + onClose: () => void; + onSubmit: () => void; +} + +export const SlashingModal = (props: UnbondModalProps) => { + return ( + + + The Finality Provider you delegated to has been slashed and removed from + the network. You can withdraw your non-slashed balance after the + timelock period ends. Slashed funds cannot be recovered. + + + ); +}; diff --git a/src/app/components/Modals/StakeModal.tsx b/src/app/components/Modals/StakeModal.tsx new file mode 100644 index 00000000..16c48c38 --- /dev/null +++ b/src/app/components/Modals/StakeModal.tsx @@ -0,0 +1,32 @@ +import { MdEditNote } from "react-icons/md"; + +import { SubmitModal } from "./SubmitModal"; + +interface StakeModalProps { + processing?: boolean; + open: boolean; + onClose: () => void; + onSubmit: () => void; +} + +export function StakeModal({ + processing, + open, + onClose, + onSubmit, +}: StakeModalProps) { + return ( + } + title="Sign and Send to BTC" + onClose={onClose} + onSubmit={onSubmit} + > + Lorem ipsum dolor sit amet consectetur. Eget ut sagittis vitae hendrerit + tempus non pellentesque. Amet enim justo vel quam pharetra sem. Id in arcu + dignissim. + + ); +} diff --git a/src/app/components/Modals/SubmitModal.tsx b/src/app/components/Modals/SubmitModal.tsx new file mode 100644 index 00000000..a6ab522e --- /dev/null +++ b/src/app/components/Modals/SubmitModal.tsx @@ -0,0 +1,80 @@ +import { + Button, + DialogBody, + DialogFooter, + Heading, + Loader, + Text, +} from "@babylonlabs-io/bbn-core-ui"; +import type { JSX, PropsWithChildren } from "react"; +import { twMerge } from "tailwind-merge"; + +import { ResponsiveDialog } from "./ResponsiveDialog"; + +interface SubmitModalProps { + className?: string; + processing?: boolean; + open: boolean; + icon: JSX.Element; + title: string; + buttons?: { + cancel?: string; + submit?: string; + }; + onClose: () => void; + onSubmit: () => void; +} + +const DEFAULT_BUTTONS = { + cancel: "Cancel", + submit: "Submit", +}; + +export const SubmitModal = ({ + className, + processing = false, + icon, + title, + children, + open, + buttons, + onClose, + onSubmit, +}: PropsWithChildren) => ( + + +
+ {icon} +
+ + + {title} + + + {children} +
+ + + + + + +
+); diff --git a/src/app/components/Modals/TransitionModal/StageStepping.tsx b/src/app/components/Modals/TransitionModal/StageStepping.tsx index ed4f4ba5..24b6e9f0 100644 --- a/src/app/components/Modals/TransitionModal/StageStepping.tsx +++ b/src/app/components/Modals/TransitionModal/StageStepping.tsx @@ -6,9 +6,8 @@ import { Loader, Text, } from "@babylonlabs-io/bbn-core-ui"; -import { twMerge } from "tailwind-merge"; -import { Tick } from "./Tick"; +import { Step } from "./Step"; const stepContent = [ "Step 1: Consent to slashing", @@ -23,37 +22,7 @@ interface StageSteppingProps { step: number; awaitingResponse?: boolean; } -const Step = ({ - active, - completed, - current, - content, -}: { - active: boolean; - completed: boolean; - current: number; - content: string; -}) => ( -
- {completed ? ( - - ) : ( -
- - {current} - -
- )} - - {content} - -
-); + export function StageStepping({ onClose, onSign, diff --git a/src/app/components/Modals/TransitionModal/Step.tsx b/src/app/components/Modals/TransitionModal/Step.tsx new file mode 100644 index 00000000..2a36d96a --- /dev/null +++ b/src/app/components/Modals/TransitionModal/Step.tsx @@ -0,0 +1,36 @@ +import { Text } from "@babylonlabs-io/bbn-core-ui"; +import { twMerge } from "tailwind-merge"; + +import { Tick } from "./Tick"; + +export const Step = ({ + active, + completed, + current, + content, +}: { + active: boolean; + completed: boolean; + current: number; + content: string; +}) => ( +
+ {completed ? ( + + ) : ( +
+ + {current} + +
+ )} + + {content} + +
+); diff --git a/src/app/components/Modals/UnbondErrorModal.tsx b/src/app/components/Modals/UnbondErrorModal.tsx index d8d0a3af..0a4e223f 100644 --- a/src/app/components/Modals/UnbondErrorModal.tsx +++ b/src/app/components/Modals/UnbondErrorModal.tsx @@ -1,16 +1,15 @@ import { Button, - Dialog, DialogBody, DialogFooter, Heading, - MobileDialog, Text, } from "@babylonlabs-io/bbn-core-ui"; import Image from "next/image"; import warningIcon from "@/app/assets/warning-icon.svg"; -import { useIsMobileView } from "@/app/hooks/useBreakpoint"; + +import { ResponsiveDialog } from "./ResponsiveDialog"; interface UnbondErrorModalProps { isOpen: boolean; @@ -19,12 +18,8 @@ interface UnbondErrorModalProps { } export const UnbondErrorModal = ({ isOpen, onDone }: UnbondErrorModalProps) => { - const isMobileView = useIsMobileView(); - - const DialogComponent = isMobileView ? MobileDialog : Dialog; - return ( - + Warning @@ -41,6 +36,6 @@ export const UnbondErrorModal = ({ isOpen, onDone }: UnbondErrorModalProps) => { Done - + ); }; diff --git a/src/app/components/Modals/UnbondModal.tsx b/src/app/components/Modals/UnbondModal.tsx index 2a6396da..1ae99d5a 100644 --- a/src/app/components/Modals/UnbondModal.tsx +++ b/src/app/components/Modals/UnbondModal.tsx @@ -1,74 +1,34 @@ -import { - Button, - Dialog, - DialogBody, - DialogFooter, - DialogHeader, - Loader, - MobileDialog, - Text, -} from "@babylonlabs-io/bbn-core-ui"; +import { Text } from "@babylonlabs-io/bbn-core-ui"; -import { useIsMobileView } from "@/app/hooks/useBreakpoint"; +import { DelegationV2 } from "@/app/types/delegationsV2"; +import { getNetworkConfig } from "@/config/network.config"; +import { satoshiToBtc } from "@/utils/btc"; + +import { ConfirmationModal } from "./ConfirmationModal"; + +const config = getNetworkConfig(); interface UnbondModalProps { - isOpen: boolean; + processing: boolean; + open: boolean; + delegation: DelegationV2 | null; onClose: () => void; - onProceed: () => void; - awaitingWalletResponse: boolean; + onSubmit: () => void; } -export const UnbondModal = ({ - isOpen, - onClose, - onProceed, - awaitingWalletResponse, -}: UnbondModalProps) => { - const isMobileView = useIsMobileView(); - - const DialogComponent = isMobileView ? MobileDialog : Dialog; - +export const UnbondModal = ({ delegation, ...props }: UnbondModalProps) => { return ( - - - - - You are about to unbond your stake before its expiration. A - transaction fee of 0.00005 Signet BTC will be deduced from your stake - by the BTC signet network. -
-
- The expected unbonding time will be about 7 days. After unbonded, you - will need to use this dashboard to withdraw your stake for it to - appear in your wallet. -
-
- - - - -
+ + + You are about to unbond your stake before its expiration. A transaction + fee of {satoshiToBtc(delegation?.stakingAmount ?? 0)} {config.coinName}{" "} + will be deduced from your stake by the BTC signet network. +
+
+ The expected unbonding time will be about 7 days. After unbonded, you + will need to use this dashboard to withdraw your stake for it to appear + in your wallet. +
+
); }; diff --git a/src/app/components/Modals/WithdrawModal.tsx b/src/app/components/Modals/WithdrawModal.tsx index cb5e5e5f..d7047b8c 100644 --- a/src/app/components/Modals/WithdrawModal.tsx +++ b/src/app/components/Modals/WithdrawModal.tsx @@ -1,75 +1,25 @@ -import { - Button, - Dialog, - DialogBody, - DialogFooter, - DialogHeader, - Loader, - MobileDialog, - Text, -} from "@babylonlabs-io/bbn-core-ui"; +import { Text } from "@babylonlabs-io/bbn-core-ui"; -import { useIsMobileView } from "@/app/hooks/useBreakpoint"; import { getNetworkConfig } from "@/config/network.config"; -export const MODE_TRANSITION = "transition"; -export const MODE_WITHDRAW = "withdraw"; -export type MODE = typeof MODE_TRANSITION | typeof MODE_WITHDRAW; +import { ConfirmationModal } from "./ConfirmationModal"; interface WithdrawModalProps { - isOpen: boolean; + processing: boolean; + open: boolean; onClose: () => void; - onProceed: () => void; - awaitingWalletResponse: boolean; + onSubmit: () => void; } const { networkName } = getNetworkConfig(); -export const WithdrawModal = ({ - isOpen, - onClose, - onProceed, - awaitingWalletResponse, -}: WithdrawModalProps) => { - const title = "Withdraw"; - const content = ( - <> - You are about to withdraw your stake.
A transaction fee will be - deduced from your stake by the {networkName} network - - ); - - const isMobileView = useIsMobileView(); - - const DialogComponent = isMobileView ? MobileDialog : Dialog; - +export const WithdrawModal = (props: WithdrawModalProps) => { return ( - - - - {content} - - - - - - + + + You are about to withdraw your stake.
A transaction fee will be + deduced from your stake by the {networkName} network +
+
); }; diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index bf518f8d..e3e74fd4 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -235,6 +235,7 @@ export const Staking = () => { stakingAmount: stakingAmountSat, stakingTxHashHex, startHeight: 0, + stakingTime: 0, state: DelegationV2StakingState.INTERMEDIATE_PENDING_VERIFICATION, }); @@ -266,43 +267,36 @@ export const Staking = () => { // Memoize the staking fee calculation const stakingFeeSat = useMemo(() => { if ( - btcWalletNetwork && - address && - latestParam && - publicKeyNoCoord && - stakingAmountSat && - finalityProvider && - mempoolFeeRates && - availableUTXOs + !( + btcWalletNetwork && + address && + latestParam && + publicKeyNoCoord && + stakingAmountSat && + finalityProvider && + mempoolFeeRates && + availableUTXOs + ) ) { - try { - // check that selected Fee rate (if present) is bigger than the min fee - if (selectedFeeRate && selectedFeeRate < minFeeRate) { - throw new Error("Selected fee rate is lower than the hour fee"); - } - const memoizedFeeRate = selectedFeeRate || defaultFeeRate; - - const eoiInput = { - finalityProviderPkNoCoordHex: finalityProvider.btcPk, - stakingAmountSat, - stakingTimelock, - feeRate: memoizedFeeRate, - }; - // Calculate the staking fee - return estimateStakingFee(eoiInput, memoizedFeeRate); - } catch (error: Error | any) { - let errorMsg = error?.message; - // Turn the error message into a user-friendly message - // The btc-staking-ts lib will be improved to return propert error types - // in the future. For now, we need to handle the errors manually by - // matching the error message. - if (errorMsg.includes("Insufficient funds")) { - errorMsg = - "Not enough balance to cover staking amount and fees, please lower the staking amount"; - } - return 0; + return 0; + } + + try { + // check that selected Fee rate (if present) is bigger than the min fee + if (selectedFeeRate && selectedFeeRate < minFeeRate) { + throw new Error("Selected fee rate is lower than the hour fee"); } - } else { + const memoizedFeeRate = selectedFeeRate || defaultFeeRate; + + const eoiInput = { + finalityProviderPkNoCoordHex: finalityProvider.btcPk, + stakingAmountSat, + stakingTimelock, + feeRate: memoizedFeeRate, + }; + // Calculate the staking fee + return estimateStakingFee(eoiInput, memoizedFeeRate); + } catch { return 0; } }, [ diff --git a/src/app/hooks/services/useDelegationService.ts b/src/app/hooks/services/useDelegationService.ts index efb51214..84894747 100644 --- a/src/app/hooks/services/useDelegationService.ts +++ b/src/app/hooks/services/useDelegationService.ts @@ -1,8 +1,11 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { DELEGATION_ACTIONS as ACTIONS } from "@/app/constants"; import { useDelegationV2State } from "@/app/state/DelegationV2State"; -import { DelegationV2StakingState as State } from "@/app/types/delegationsV2"; +import { + DelegationV2, + DelegationV2StakingState as State, +} from "@/app/types/delegationsV2"; import { useTransactionService } from "./useTransactionService"; @@ -29,13 +32,23 @@ interface TxProps { type DelegationCommand = (props: TxProps) => Promise; +interface ConfirmationModalState { + action: ActionType; + delegation: DelegationV2; +} + export function useDelegationService() { + const [confirmationModal, setConfirmationModal] = + useState(null); + const [processingDelegations, setProcessingDelegations] = useState< + Record + >({}); + const { delegations = [], fetchMoreDelegations, hasMoreDelegations, isLoading, - findDelegationByTxHash, updateDelegationStatus, } = useDelegationV2State(); @@ -46,6 +59,14 @@ export function useDelegationService() { submitTimelockUnbondedWithdrawalTx, } = useTransactionService(); + const processing = useMemo( + () => + confirmationModal?.delegation + ? processingDelegations[confirmationModal.delegation.stakingTxHashHex] + : false, + [confirmationModal, processingDelegations], + ); + const COMMANDS: Record = useMemo( () => ({ [ACTIONS.STAKE]: async ({ @@ -114,57 +135,59 @@ export function useDelegationService() { ); }, - [ACTIONS.WITHDRAW_ON_TIMELOCK]: async ({ + [ACTIONS.WITHDRAW_ON_EARLY_UNBOUNDING_SLASHING]: async ({ + stakingTxHashHex, stakingInput, paramsVersion, - stakingTxHex, - stakingTxHashHex, - }: TxProps) => { - await submitTimelockUnbondedWithdrawalTx( + unbondingSlashingTxHex, + }) => { + if (!unbondingSlashingTxHex) { + throw new Error( + "Unbonding slashing tx not found, can't submit withdrawal", + ); + } + + await submitEarlyUnbondedWithdrawalTx( stakingInput, paramsVersion, - stakingTxHex, + unbondingSlashingTxHex, ); updateDelegationStatus( stakingTxHashHex, - State.INTERMEDIATE_TIMELOCK_WITHDRAWAL_SUBMITTED, + State.INTERMEDIATE_EARLY_UNBONDING_SLASHING_WITHDRAWAL_SUBMITTED, ); }, - [ACTIONS.WITHDRAW_ON_EARLY_UNBOUNDING_SLASHING]: async ({ + [ACTIONS.WITHDRAW_ON_TIMELOCK]: async ({ stakingInput, paramsVersion, - unbondingSlashingTxHex, stakingTxHashHex, - }) => { - if (!unbondingSlashingTxHex) { - throw new Error( - "Unbonding slashing tx not found, can't submit withdrawal", - ); - } - await submitEarlyUnbondedWithdrawalTx( + stakingTxHex, + }: TxProps) => { + await submitTimelockUnbondedWithdrawalTx( stakingInput, paramsVersion, - unbondingSlashingTxHex, + stakingTxHex, ); updateDelegationStatus( stakingTxHashHex, - State.INTERMEDIATE_EARLY_UNBONDING_SLASHING_WITHDRAWAL_SUBMITTED, + State.INTERMEDIATE_TIMELOCK_WITHDRAWAL_SUBMITTED, ); }, [ACTIONS.WITHDRAW_ON_TIMELOCK_SLASHING]: async ({ stakingInput, paramsVersion, - slashingTxHex, stakingTxHashHex, + slashingTxHex, }) => { if (!slashingTxHex) { throw new Error("Slashing tx not found, can't submit withdrawal"); } - await submitEarlyUnbondedWithdrawalTx( + + await submitTimelockUnbondedWithdrawalTx( stakingInput, paramsVersion, slashingTxHex, @@ -172,7 +195,7 @@ export function useDelegationService() { updateDelegationStatus( stakingTxHashHex, - State.INTERMEDIATE_TIMELOCK_WITHDRAWAL_SUBMITTED, + State.INTERMEDIATE_TIMELOCK_SLASHING_WITHDRAWAL_SUBMITTED, ); }, }), @@ -185,14 +208,30 @@ export function useDelegationService() { ], ); - const executeDelegationAction = useCallback( - async (action: string, txHash: string) => { - const d = findDelegationByTxHash(txHash); + const openConfirmationModal = useCallback( + (action: ActionType, delegation: DelegationV2) => { + setConfirmationModal({ + action, + delegation, + }); + }, + [], + ); - if (!d) { - throw new Error("Delegation not found: " + txHash); - } + const closeConfirmationModal = useCallback( + () => void setConfirmationModal(null), + [], + ); + const toggleProcessingDelegation = useCallback( + (id: string, processing: boolean) => { + setProcessingDelegations((state) => ({ ...state, [id]: processing })); + }, + [], + ); + + const executeDelegationAction = useCallback( + async (action: string, delegation: DelegationV2) => { const { stakingTxHashHex, stakingTxHex, @@ -205,7 +244,7 @@ export function useDelegationService() { state, slashingTxHex, unbondingSlashingTxHex, - } = d; + } = delegation; const finalityProviderPk = finalityProviderBtcPksHex[0]; const stakingInput = { @@ -216,25 +255,37 @@ export function useDelegationService() { const execute = COMMANDS[action as ActionType]; - await execute?.({ - stakingTxHashHex, - stakingTxHex, - paramsVersion, - unbondingTxHex, - covenantUnbondingSignatures, - state, - stakingInput, - slashingTxHex, - unbondingSlashingTxHex, - }); + try { + toggleProcessingDelegation(stakingTxHashHex, true); + + await execute?.({ + stakingTxHashHex, + stakingTxHex, + paramsVersion, + unbondingTxHex, + covenantUnbondingSignatures, + state, + stakingInput, + slashingTxHex, + unbondingSlashingTxHex, + }); + + closeConfirmationModal(); + } finally { + toggleProcessingDelegation(stakingTxHashHex, false); + } }, - [COMMANDS, findDelegationByTxHash], + [COMMANDS, closeConfirmationModal, toggleProcessingDelegation], ); return { + processing, isLoading, delegations, hasMoreDelegations, + confirmationModal, + openConfirmationModal, + closeConfirmationModal, fetchMoreDelegations, executeDelegationAction, }; diff --git a/src/app/hooks/storage/useDelegationStorage.ts b/src/app/hooks/storage/useDelegationStorage.ts index 59c1a05e..a05f5541 100644 --- a/src/app/hooks/storage/useDelegationStorage.ts +++ b/src/app/hooks/storage/useDelegationStorage.ts @@ -82,11 +82,14 @@ export function useDelegationStorage( setDelegationStatuses((statuses) => Object.entries(statuses) - .filter( - ([hash, status]) => + .filter(([hash, status]) => { + if (!delegationMap[hash]?.state) return true; + + return ( DELEGATION_STATUSES[delegationMap[hash].state] < - DELEGATION_STATUSES[status], - ) + DELEGATION_STATUSES[status] + ); + }) .reduce( (acc, [hash, status]) => ({ ...acc, [hash]: status }), {} as Record, diff --git a/src/app/types/delegationsV2.ts b/src/app/types/delegationsV2.ts index 62fad34b..3e2013da 100644 --- a/src/app/types/delegationsV2.ts +++ b/src/app/types/delegationsV2.ts @@ -2,6 +2,7 @@ export interface DelegationLike { stakingAmount: number; stakingTxHashHex: string; startHeight: number; + stakingTime: number; state: DelegationV2StakingState; } diff --git a/src/components/delegations/DelegationList/components/ActionButton.tsx b/src/components/delegations/DelegationList/components/ActionButton.tsx index be864081..0c3098f9 100644 --- a/src/components/delegations/DelegationList/components/ActionButton.tsx +++ b/src/components/delegations/DelegationList/components/ActionButton.tsx @@ -1,13 +1,20 @@ import { DELEGATION_ACTIONS as ACTIONS } from "@/app/constants"; -import { DelegationV2StakingState as State } from "@/app/types/delegationsV2"; +import { ActionType } from "@/app/hooks/services/useDelegationService"; +import { + DelegationV2, + DelegationV2StakingState as State, +} from "@/app/types/delegationsV2"; interface ActionButtonProps { - txHash: string; + delegation: DelegationV2; state: string; - onClick?: (action: string, txHash: string) => void; + onClick?: (action: ActionType, delegation: DelegationV2) => void; } -const ACTION_BUTTON_PROPS: Record = { +const ACTION_BUTTON_PROPS: Record< + string, + { action: ActionType; title: string } +> = { [State.VERIFIED]: { action: ACTIONS.STAKE, title: "Stake", @@ -42,7 +49,7 @@ export function ActionButton(props: ActionButtonProps) { return ( diff --git a/src/components/delegations/DelegationList/components/DelegationModal.tsx b/src/components/delegations/DelegationList/components/DelegationModal.tsx new file mode 100644 index 00000000..d280a45e --- /dev/null +++ b/src/components/delegations/DelegationList/components/DelegationModal.tsx @@ -0,0 +1,66 @@ +import { useCallback } from "react"; + +import { SlashingModal } from "@/app/components/Modals/SlashingModal"; +import { StakeModal } from "@/app/components/Modals/StakeModal"; +import { UnbondModal } from "@/app/components/Modals/UnbondModal"; +import { WithdrawModal } from "@/app/components/Modals/WithdrawModal"; +import { DELEGATION_ACTIONS as ACTIONS } from "@/app/constants"; +import { ActionType } from "@/app/hooks/services/useDelegationService"; +import { DelegationV2 } from "@/app/types/delegationsV2"; + +interface ConfirmationModalProps { + processing: boolean; + action: ActionType | undefined; + delegation: DelegationV2 | null; + onSubmit: (action: ActionType, delegation: DelegationV2) => void; + onClose: () => void; +} + +export function DelegationModal({ + action, + delegation, + onSubmit, + ...restProps +}: ConfirmationModalProps) { + const handleSubmit = useCallback(() => { + if (!action || !delegation) return; + + onSubmit(action, delegation); + }, [action, delegation, onSubmit]); + + return ( + <> + + + + + + + + ); +} diff --git a/src/components/delegations/DelegationList/components/Inception.tsx b/src/components/delegations/DelegationList/components/Inception.tsx index 41ea9d77..80622d2e 100644 --- a/src/components/delegations/DelegationList/components/Inception.tsx +++ b/src/components/delegations/DelegationList/components/Inception.tsx @@ -6,6 +6,7 @@ interface Inception { export function Inception({ value }: Inception) { const currentTime = Date.now(); + return (
{durationTillNow(value, currentTime)} diff --git a/src/components/delegations/DelegationList/index.tsx b/src/components/delegations/DelegationList/index.tsx index 78518623..2d12af6c 100644 --- a/src/components/delegations/DelegationList/index.tsx +++ b/src/components/delegations/DelegationList/index.tsx @@ -1,19 +1,23 @@ import { Heading } from "@babylonlabs-io/bbn-core-ui"; -import { useDelegationService } from "@/app/hooks/services/useDelegationService"; +import { + ActionType, + useDelegationService, +} from "@/app/hooks/services/useDelegationService"; import { type DelegationV2 } from "@/app/types/delegationsV2"; import { GridTable, type TableColumn } from "@/components/common/GridTable"; import { FinalityProviderMoniker } from "@/components/delegations/DelegationList/components/FinalityProviderMoniker"; import { ActionButton } from "./components/ActionButton"; import { Amount } from "./components/Amount"; +import { DelegationModal } from "./components/DelegationModal"; import { Inception } from "./components/Inception"; import { Status } from "./components/Status"; import { TxHash } from "./components/TxHash"; const columns: TableColumn< DelegationV2, - { handleActionClick: (action: string, txHash: string) => void } + { handleActionClick: (action: ActionType, delegation: DelegationV2) => void } >[] = [ { field: "Inception", @@ -48,23 +52,29 @@ const columns: TableColumn< { field: "actions", headerName: "Action", - renderCell: (row, _, { handleActionClick }) => ( - handleActionClick(action, txHash)} - /> - ), + renderCell: (row, _, { handleActionClick }) => { + return ( + + ); + }, }, ]; export function DelegationList() { const { + processing, + confirmationModal, delegations, isLoading, hasMoreDelegations, fetchMoreDelegations, executeDelegationAction, + openConfirmationModal, + closeConfirmationModal, } = useDelegationService(); return ( @@ -72,6 +82,7 @@ export function DelegationList() { Babylon Chain Stakes (Phase 2) + `${row.stakingTxHashHex}-${row.startHeight}`} columns={columns} @@ -88,9 +99,17 @@ export function DelegationList() { cellClassName: "p-4 first:pl-4 first:rounded-l last:pr-4 last:rounded-r bg-secondary-contrast flex items-center text-sm justify-start group-even:bg-[#F9F9F9] text-primary-dark", }} - params={{ handleActionClick: executeDelegationAction }} + params={{ handleActionClick: openConfirmationModal }} fallback={
No delegations found
} /> + +
); }