Skip to content

Commit

Permalink
feat: add selegation storage
Browse files Browse the repository at this point in the history
  • Loading branch information
totraev committed Nov 29, 2024
1 parent 78ca065 commit 3d37d2a
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 74 deletions.
10 changes: 10 additions & 0 deletions src/app/components/Staking/Staking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -74,6 +76,7 @@ export const Staking = () => {
useLocalStorage<boolean>("bbn-staking-cancelFeedbackModalOpened ", false);

const { createDelegationEoi, estimateStakingFee } = useTransactionService();
const { addDelegation } = useDelegationV2State();
const { networkInfo } = useAppState();
const latestParam = networkInfo?.params.bbnStakingParams?.latestParam;
const stakingStatus = networkInfo?.stakingStatus;
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 8 additions & 1 deletion src/app/hooks/services/useDelegationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ export function useDelegationService() {
hasMoreDelegations,
isLoading,
findDelegationByTxHash,
addDelegation,
} = useDelegationV2State();

const {
submitStakingTx,
submitUnbondingTx,
submitEarlyUnbondedWithdrawalTx,
submitTimelockUnbondedWithdrawalTx,
createDelegationEoi,
} = useTransactionService();

const COMMANDS: Record<ActionType, DelegationCommand> = useMemo(
Expand Down Expand Up @@ -140,7 +142,12 @@ export function useDelegationService() {
);
},
}),
[],
[
submitStakingTx,
submitUnbondingTx,
submitEarlyUnbondedWithdrawalTx,
submitTimelockUnbondedWithdrawalTx,
],
);

const executeDelegationAction = useCallback(
Expand Down
130 changes: 130 additions & 0 deletions src/app/hooks/storage/useDelegationStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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[],
) {
const [pendingDelegations = {}, setPendingDelegations] = useLocalStorage<
Record<string, DelegationLike>
>(`${key}_pending`, {});
const [delegationStatuses = {}, setDelegationStatuses] = useLocalStorage<
Record<string, State>
>(`${key}_statuses`, {});

const delegationMap = useMemo(() => {
return (delegations ?? []).reduce(
(acc, delegation) => ({
...acc,
[delegation.stakingTxHashHex]: delegation,
}),
{} as Record<string, DelegationV2>,
);
}, [delegations]);

const formattedDelegations = useMemo(() => {
const pendingDelegationArr = Object.values(pendingDelegations).map(
(d) =>
({
...d,
stakingTxHex: "",
paramsVersion: 0,
finalityProviderBtcPksHex: [],
stakerBtcPkHex: "",
stakingTime: 0,
endHeight: 0,
unbondingTime: 0,
unbondingTxHex: "",
stakingSlashingTxHex: "",
bbnInceptionHeight: 0,
bbnInceptionTime: 0,
slashingTxHex: "",
unbondingSlashingTxHex: "",
}) 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<string, DelegationLike>,
);

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<string, State>,
),
);
},
[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,
};
}
79 changes: 18 additions & 61 deletions src/app/state/DelegationV2State.tsx
Original file line number Diff line number Diff line change
@@ -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 { 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;
}
Expand All @@ -21,8 +22,9 @@ const { StateProvider, useState } = createStateUtils<DelegationV2State>({
isLoading: false,
delegations: [],
hasMoreDelegations: false,
addDelegation: () => null,
fetchMoreDelegations: () => null,
addDelegation: () => {},
updateDelegationStatus: () => {},
fetchMoreDelegations: () => {},
findDelegationByTxHash: () => undefined,
});

Expand All @@ -32,43 +34,11 @@ export function DelegationV2State({ children }: PropsWithChildren) {
useDelegationsV2();

// States
const [delegations, setDelegations] = useLocalStorage<DelegationV2[]>(
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(
Expand All @@ -79,18 +49,20 @@ export function DelegationV2State({ children }: PropsWithChildren) {
// Context
const state = useMemo(
() => ({
delegations,
delegations: delegations,
isLoading: isFetchingNextPage,
hasMoreDelegations: hasNextPage,
addDelegation,
addDelegation: addPendingDelegation,
updateDelegationStatus,
findDelegationByTxHash,
fetchMoreDelegations: fetchNextPage,
}),
[
delegations,
isFetchingNextPage,
hasNextPage,
addDelegation,
addPendingDelegation,
updateDelegationStatus,
fetchNextPage,
findDelegationByTxHash,
],
Expand All @@ -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
);
});
}
47 changes: 43 additions & 4 deletions src/app/types/delegationsV2.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
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;
startHeight: number;
endHeight: number;
unbondingTime: number;
unbondingTxHex: string;
state: DelegationV2StakingState;
covenantUnbondingSignatures?: {
covenantBtcPkHex: string;
signatureHex: string;
Expand Down Expand Up @@ -46,14 +50,49 @@ 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.SLASHED]: 9,
[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 => {
Expand Down
Loading

0 comments on commit 3d37d2a

Please sign in to comment.