From 32c7fe37442db4455be9e326a15ff413ec8def94 Mon Sep 17 00:00:00 2001 From: David Totraev Date: Fri, 29 Nov 2024 13:42:58 +0500 Subject: [PATCH] feat: add selegation storage --- src/app/components/Staking/Staking.tsx | 10 ++ .../hooks/services/useDelegationService.ts | 31 ++++- src/app/hooks/storage/useDelegationStorage.ts | 126 ++++++++++++++++++ src/app/state/DelegationV2State.tsx | 81 +++-------- src/app/types/delegationsV2.ts | 46 ++++++- .../components/ActionButton.tsx | 14 +- .../DelegationList/components/Status.tsx | 14 +- 7 files changed, 246 insertions(+), 76 deletions(-) create mode 100644 src/app/hooks/storage/useDelegationStorage.ts diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 834cdc24..c8a30383 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -14,6 +14,8 @@ import { } from "@/app/hooks/services/useTransactionService"; import { useHealthCheck } from "@/app/hooks/useHealthCheck"; import { useAppState } from "@/app/state"; +import { useDelegationV2State } from "@/app/state/DelegationV2State"; +import { DelegationV2StakingState } from "@/app/types/delegationsV2"; import { ErrorHandlerParam, ErrorState } from "@/app/types/errors"; import { FinalityProvider, @@ -74,6 +76,7 @@ export const Staking = () => { useLocalStorage("bbn-staking-cancelFeedbackModalOpened ", false); const { createDelegationEoi, estimateStakingFee } = useTransactionService(); + const { addDelegation } = useDelegationV2State(); const { networkInfo } = useAppState(); const latestParam = networkInfo?.params.bbnStakingParams?.latestParam; const stakingStatus = networkInfo?.stakingStatus; @@ -226,6 +229,13 @@ export const Staking = () => { signingCallback, ); + addDelegation({ + stakingAmount: stakingAmountSat, + stakingTxHashHex, + startHeight: 0, + state: DelegationV2StakingState.INTERMEDIATE_PENDING_VERIFICATION, + }); + setStakingTxHashHex(stakingTxHashHex); setPendingVerificationOpen(true); } catch (error: Error | any) { diff --git a/src/app/hooks/services/useDelegationService.ts b/src/app/hooks/services/useDelegationService.ts index 0e4efd95..b9bc0491 100644 --- a/src/app/hooks/services/useDelegationService.ts +++ b/src/app/hooks/services/useDelegationService.ts @@ -2,7 +2,10 @@ import { useCallback, useMemo } from "react"; import { DELEGATION_ACTIONS as ACTIONS } from "@/app/constants"; import { useDelegationV2State } from "@/app/state/DelegationV2State"; -import type { DelegationV2StakingState } from "@/app/types/delegationsV2"; +import type { + DelegationLike, + DelegationV2StakingState, +} from "@/app/types/delegationsV2"; import { useTransactionService } from "./useTransactionService"; @@ -39,6 +42,7 @@ export function useDelegationService() { hasMoreDelegations, isLoading, findDelegationByTxHash, + addDelegation, } = useDelegationV2State(); const { @@ -46,6 +50,7 @@ export function useDelegationService() { submitUnbondingTx, submitEarlyUnbondedWithdrawalTx, submitTimelockUnbondedWithdrawalTx, + createDelegationEoi, } = useTransactionService(); const COMMANDS: Record = useMemo( @@ -140,7 +145,12 @@ export function useDelegationService() { ); }, }), - [], + [ + submitStakingTx, + submitUnbondingTx, + submitEarlyUnbondedWithdrawalTx, + submitTimelockUnbondedWithdrawalTx, + ], ); const executeDelegationAction = useCallback( @@ -192,6 +202,23 @@ export function useDelegationService() { [COMMANDS, findDelegationByTxHash], ); + const submitDelegationEOI = useCallback( + async ({ + stakingAmount, + stakingTxHashHex, + startHeight, + state, + }: DelegationLike) => { + addDelegation({ + stakingAmount, + stakingTxHashHex, + startHeight, + state, + }); + }, + [addDelegation, createDelegationEoi], + ); + return { isLoading, delegations, diff --git a/src/app/hooks/storage/useDelegationStorage.ts b/src/app/hooks/storage/useDelegationStorage.ts new file mode 100644 index 00000000..59cc886c --- /dev/null +++ b/src/app/hooks/storage/useDelegationStorage.ts @@ -0,0 +1,126 @@ +import { useCallback, useEffect, useMemo } from "react"; +import { useLocalStorage } from "usehooks-ts"; + +import { + DELEGATION_STATUSES, + DelegationLike, + DelegationV2, + DelegationV2StakingState as State, +} from "@/app/types/delegationsV2"; + +export function useDelegationStorage( + key: string, + delegations?: DelegationV2[], +) { + console.log(delegations); + const [pendingDelegations = {}, setPendingDelegations] = useLocalStorage< + Record + >(`${key}_pending`, {}); + const [delegationStatuses = {}, setDelegationStatuses] = useLocalStorage< + Record + >(`${key}_statuses`, {}); + + const delegationMap = useMemo(() => { + return (delegations ?? []).reduce( + (acc, delegation) => ({ + ...acc, + [delegation.stakingTxHashHex]: delegation, + }), + {} as Record, + ); + }, [delegations]); + + const formattedDelegations = useMemo(() => { + const pendingDelegationArr = Object.values(pendingDelegations).map( + (d) => + ({ + ...d, + stakingTxHex: "", + paramsVersion: 0, + finalityProviderBtcPksHex: [], + stakerBtcPkHex: "", + stakingTime: 0, + endHeight: 0, + unbondingTime: 0, + unbondingTxHex: "", + }) as DelegationV2, + ); + + return pendingDelegationArr.concat( + (delegations ?? []) + .filter((d) => !pendingDelegations[d.stakingTxHashHex]) + .map((d) => ({ + ...d, + state: delegationStatuses[d.stakingTxHashHex] ?? d.state, + })), + ); + }, [delegations, pendingDelegations, delegationStatuses]); + + useEffect( + function syncPendingDelegations() { + if (!key) return; + + setPendingDelegations((delegations) => { + const result = Object.values(delegations) + .filter((d) => !delegationMap[d.stakingTxHashHex]) + .reduce( + (acc, d) => ({ ...acc, [d.stakingTxHashHex]: d }), + {} as Record, + ); + + return result; + }); + }, + [key, delegationMap, setPendingDelegations], + ); + + useEffect( + function syncDelegationStatuses() { + if (!key) return; + + setDelegationStatuses((statuses) => + Object.entries(statuses) + .filter( + ([hash, status]) => + DELEGATION_STATUSES[status] < + DELEGATION_STATUSES[delegationMap[hash].state], + ) + .reduce( + (acc, [hash, status]) => ({ ...acc, [hash]: status }), + {} as Record, + ), + ); + }, + [key, delegationMap, setDelegationStatuses], + ); + + const addPendingDelegation = useCallback( + (delegation: DelegationLike) => { + if (!key) return; + + setPendingDelegations((delegations) => ({ + ...delegations, + [delegation.stakingTxHashHex]: { + ...delegation, + state: State.INTERMEDIATE_PENDING_VERIFICATION, + }, + })); + }, + [key, setPendingDelegations], + ); + + const updateDelegationStatus = useCallback( + (id: string, status: State) => { + if (!key) return; + + setDelegationStatuses((statuses) => ({ ...statuses, [id]: status })); + }, + [key, setDelegationStatuses], + ); + + return { + delegations: formattedDelegations, + addPendingDelegation, + updateDelegationStatus, + }; +} diff --git a/src/app/state/DelegationV2State.tsx b/src/app/state/DelegationV2State.tsx index d1078b7a..fc802192 100644 --- a/src/app/state/DelegationV2State.tsx +++ b/src/app/state/DelegationV2State.tsx @@ -1,18 +1,19 @@ -import { useCallback, useEffect, useMemo, type PropsWithChildren } from "react"; -import { useLocalStorage } from "usehooks-ts"; +import { useCallback, useMemo, type PropsWithChildren } from "react"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; -import type { DelegationV2 } from "@/app/types/delegationsV2"; +import type { DelegationLike, DelegationV2 } from "@/app/types/delegationsV2"; import { createStateUtils } from "@/utils/createStateUtils"; -import { getDelegationsV2LocalStorageKey } from "@/utils/local_storage/getDelegationsLocalStorageKey"; +import { getDelegationsV2LocalStorageKey } from "@/utils/local_storage/getDelegationsLocalStorageKey"; import { useDelegationsV2 } from "../hooks/api/useDelegationsV2"; +import { useDelegationStorage } from "../hooks/storage/useDelegationStorage"; interface DelegationV2State { isLoading: boolean; hasMoreDelegations: boolean; delegations: DelegationV2[]; - addDelegation: (delegation: DelegationV2) => void; + addDelegation: (delegation: DelegationLike) => void; + updateDelegationStatus: (is: string, status: DelegationV2["state"]) => void; fetchMoreDelegations: () => void; findDelegationByTxHash: (txHash: string) => DelegationV2 | undefined; } @@ -21,8 +22,9 @@ const { StateProvider, useState } = createStateUtils({ isLoading: false, delegations: [], hasMoreDelegations: false, - addDelegation: () => null, - fetchMoreDelegations: () => null, + addDelegation: () => {}, + updateDelegationStatus: () => {}, + fetchMoreDelegations: () => {}, findDelegationByTxHash: () => undefined, }); @@ -32,43 +34,11 @@ export function DelegationV2State({ children }: PropsWithChildren) { useDelegationsV2(); // States - const [delegations, setDelegations] = useLocalStorage( - getDelegationsV2LocalStorageKey(publicKeyNoCoord), - [], - ); - - // Effects - useEffect( - function syncDelegations() { - if (!data?.delegations) { - return; - } - // TODO: Find the difference and update only the difference - if (!areDelegationsEqual(delegations, data.delegations)) { - setDelegations(data.delegations); - } - }, - [data?.delegations, delegations, setDelegations], - ); - - // Methods - const addDelegation = useCallback( - (newDelegation: DelegationV2) => { - setDelegations((delegations) => { - const exists = delegations.some( - (delegation) => - delegation.stakingTxHashHex === newDelegation.stakingTxHashHex, - ); - - if (!exists) { - return [newDelegation, ...delegations]; - } - - return delegations; - }); - }, - [setDelegations], - ); + const { delegations, addPendingDelegation, updateDelegationStatus } = + useDelegationStorage( + getDelegationsV2LocalStorageKey(publicKeyNoCoord), + data?.delegations, + ); // Get a delegation by its txHash const findDelegationByTxHash = useCallback( @@ -79,10 +49,11 @@ export function DelegationV2State({ children }: PropsWithChildren) { // Context const state = useMemo( () => ({ - delegations, + delegations: delegations, isLoading: isFetchingNextPage, hasMoreDelegations: hasNextPage, - addDelegation, + addDelegation: addPendingDelegation, + updateDelegationStatus, findDelegationByTxHash, fetchMoreDelegations: fetchNextPage, }), @@ -90,7 +61,8 @@ export function DelegationV2State({ children }: PropsWithChildren) { delegations, isFetchingNextPage, hasNextPage, - addDelegation, + addPendingDelegation, + updateDelegationStatus, fetchNextPage, findDelegationByTxHash, ], @@ -100,18 +72,3 @@ export function DelegationV2State({ children }: PropsWithChildren) { } export const useDelegationV2State = useState; - -function areDelegationsEqual( - arr1: DelegationV2[], - arr2: DelegationV2[], -): boolean { - if (arr1.length !== arr2.length) return false; - - return arr1.every((item, index) => { - const other = arr2[index]; - return ( - item.stakingTxHashHex === other.stakingTxHashHex && - item.state === other.state - ); - }); -} diff --git a/src/app/types/delegationsV2.ts b/src/app/types/delegationsV2.ts index 00ab57df..4b908cd9 100644 --- a/src/app/types/delegationsV2.ts +++ b/src/app/types/delegationsV2.ts @@ -1,11 +1,16 @@ -export interface DelegationV2 { +export interface DelegationLike { + stakingAmount: number; stakingTxHashHex: string; + startHeight: number; + state: DelegationV2StakingState; +} + +export interface DelegationV2 extends DelegationLike { stakingTxHex: string; stakingSlashingTxHex: string; paramsVersion: number; finalityProviderBtcPksHex: string[]; stakerBtcPkHex: string; - stakingAmount: number; stakingTime: number; bbnInceptionHeight: number; bbnInceptionTime: number; @@ -13,7 +18,6 @@ export interface DelegationV2 { endHeight: number; unbondingTime: number; unbondingTxHex: string; - state: DelegationV2StakingState; covenantUnbondingSignatures?: { covenantBtcPkHex: string; signatureHex: string; @@ -46,14 +50,48 @@ export enum DelegationV2StakingState { // Slashed states SLASHED = "SLASHED", + EARLY_UNBONDING_SLASHED = "EARLY_UNBONDING_SLASHED", + TIMELOCK_SLASHED = "TIMELOCK_SLASHED", // Intermediate states INTERMEDIATE_PENDING_VERIFICATION = "INTERMEDIATE_PENDING_VERIFICATION", INTERMEDIATE_PENDING_BTC_CONFIRMATION = "INTERMEDIATE_PENDING_BTC_CONFIRMATION", INTERMEDIATE_UNBONDING_SUBMITTED = "INTERMEDIATE_UNBONDING_SUBMITTED", - INTERMEDIATE_WITHDRAWAL_SUBMITTED = "INTERMEDIATE_WITHDRAWAL_SUBMITTED", + INTERMEDIATE_EARLY_UNBONDING_WITHDRAWAL_SUBMITTED = "INTERMEDIATE_EARLY_UNBONDING_WITHDRAWAL_SUBMITTED", + INTERMEDIATE_EARLY_UNBONDING_SLASHING_WITHDRAWAL_SUBMITTED = "INTERMEDIATE_EARLY_UNBONDING_SLASHING_WITHDRAWAL_SUBMITTED", + INTERMEDIATE_TIMELOCK_WITHDRAWAL_SUBMITTED = "INTERMEDIATE_TIMELOCK_WITHDRAWAL_SUBMITTED", + INTERMEDIATE_TIMELOCK_SLASHING_WITHDRAWAL_SUBMITTED = "INTERMEDIATE_TIMELOCK_SLASHING_WITHDRAWAL_SUBMITTED", } +export const DELEGATION_STATUSES = { + [DelegationV2StakingState.PENDING]: 0, + [DelegationV2StakingState.INTERMEDIATE_PENDING_VERIFICATION]: 0, + [DelegationV2StakingState.VERIFIED]: 1, + [DelegationV2StakingState.INTERMEDIATE_PENDING_BTC_CONFIRMATION]: 2, + [DelegationV2StakingState.ACTIVE]: 3, + + [DelegationV2StakingState.INTERMEDIATE_UNBONDING_SUBMITTED]: 4, + [DelegationV2StakingState.EARLY_UNBONDING]: 5, + [DelegationV2StakingState.EARLY_UNBONDING_WITHDRAWABLE]: 6, + [DelegationV2StakingState.INTERMEDIATE_EARLY_UNBONDING_WITHDRAWAL_SUBMITTED]: 7, + [DelegationV2StakingState.EARLY_UNBONDING_WITHDRAWN]: 8, + + [DelegationV2StakingState.EARLY_UNBONDING_SLASHED]: 9, + [DelegationV2StakingState.EARLY_UNBONDING_SLASHING_WITHDRAWABLE]: 10, + [DelegationV2StakingState.INTERMEDIATE_EARLY_UNBONDING_SLASHING_WITHDRAWAL_SUBMITTED]: 11, + [DelegationV2StakingState.EARLY_UNBONDING_SLASHING_WITHDRAWN]: 12, + + [DelegationV2StakingState.TIMELOCK_UNBONDING]: 13, + [DelegationV2StakingState.TIMELOCK_WITHDRAWABLE]: 14, + [DelegationV2StakingState.INTERMEDIATE_TIMELOCK_WITHDRAWAL_SUBMITTED]: 15, + [DelegationV2StakingState.TIMELOCK_WITHDRAWN]: 16, + + [DelegationV2StakingState.TIMELOCK_SLASHED]: 17, + [DelegationV2StakingState.TIMELOCK_SLASHING_WITHDRAWABLE]: 18, + [DelegationV2StakingState.INTERMEDIATE_TIMELOCK_SLASHING_WITHDRAWAL_SUBMITTED]: 19, + [DelegationV2StakingState.TIMELOCK_SLASHING_WITHDRAWN]: 20, +} as const; + export const getDelegationV2StakingState = ( state: string, ): DelegationV2StakingState => { diff --git a/src/components/delegations/DelegationList/components/ActionButton.tsx b/src/components/delegations/DelegationList/components/ActionButton.tsx index cada3caf..be864081 100644 --- a/src/components/delegations/DelegationList/components/ActionButton.tsx +++ b/src/components/delegations/DelegationList/components/ActionButton.tsx @@ -1,5 +1,5 @@ import { DELEGATION_ACTIONS as ACTIONS } from "@/app/constants"; -import { DelegationV2StakingState as state } from "@/app/types/delegationsV2"; +import { DelegationV2StakingState as State } from "@/app/types/delegationsV2"; interface ActionButtonProps { txHash: string; @@ -8,27 +8,27 @@ interface ActionButtonProps { } const ACTION_BUTTON_PROPS: Record = { - [state.VERIFIED]: { + [State.VERIFIED]: { action: ACTIONS.STAKE, title: "Stake", }, - [state.ACTIVE]: { + [State.ACTIVE]: { action: ACTIONS.UNBOUND, title: "Unbound", }, - [state.EARLY_UNBONDING_WITHDRAWABLE]: { + [State.EARLY_UNBONDING_WITHDRAWABLE]: { action: ACTIONS.WITHDRAW_ON_EARLY_UNBOUNDING, title: "Withdraw", }, - [state.TIMELOCK_WITHDRAWABLE]: { + [State.TIMELOCK_WITHDRAWABLE]: { action: ACTIONS.WITHDRAW_ON_TIMELOCK, title: "Withdraw", }, - [state.TIMELOCK_SLASHING_WITHDRAWABLE]: { + [State.TIMELOCK_SLASHING_WITHDRAWABLE]: { action: ACTIONS.WITHDRAW_ON_TIMELOCK_SLASHING, title: "Withdraw", }, - [state.EARLY_UNBONDING_SLASHING_WITHDRAWABLE]: { + [State.EARLY_UNBONDING_SLASHING_WITHDRAWABLE]: { action: ACTIONS.WITHDRAW_ON_EARLY_UNBOUNDING_SLASHING, title: "Withdraw", }, diff --git a/src/components/delegations/DelegationList/components/Status.tsx b/src/components/delegations/DelegationList/components/Status.tsx index 7252daf9..47306065 100644 --- a/src/components/delegations/DelegationList/components/Status.tsx +++ b/src/components/delegations/DelegationList/components/Status.tsx @@ -81,7 +81,19 @@ const STATUSES: Record< label: "Unbonding", tooltip: "Stake is requesting unbonding", }), - [state.INTERMEDIATE_WITHDRAWAL_SUBMITTED]: () => ({ + [state.INTERMEDIATE_EARLY_UNBONDING_WITHDRAWAL_SUBMITTED]: () => ({ + label: "Withdrawal", + tooltip: "Withdrawal transaction pending confirmation on Bitcoin", + }), + [state.INTERMEDIATE_EARLY_UNBONDING_SLASHING_WITHDRAWAL_SUBMITTED]: () => ({ + label: "Withdrawal", + tooltip: "Withdrawal transaction pending confirmation on Bitcoin", + }), + [state.INTERMEDIATE_TIMELOCK_WITHDRAWAL_SUBMITTED]: () => ({ + label: "Withdrawal", + tooltip: "Withdrawal transaction pending confirmation on Bitcoin", + }), + [state.INTERMEDIATE_TIMELOCK_SLASHING_WITHDRAWAL_SUBMITTED]: () => ({ label: "Withdrawal", tooltip: "Withdrawal transaction pending confirmation on Bitcoin", }),