From 3ba2c38c7e9ec20a875362af1938b319d6d02419 Mon Sep 17 00:00:00 2001 From: David Totraev Date: Thu, 28 Nov 2024 08:59:37 +0500 Subject: [PATCH 1/4] feat: add delegation service --- src/app/constants.ts | 10 + .../hooks/services/useDelegationService.ts | 202 ++++++++++++++++++ .../components/ActionButton.tsx | 75 +++---- .../delegations/DelegationList/index.tsx | 120 +---------- 4 files changed, 256 insertions(+), 151 deletions(-) create mode 100644 src/app/hooks/services/useDelegationService.ts diff --git a/src/app/constants.ts b/src/app/constants.ts index be40975f..29842c6e 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -1,2 +1,12 @@ export const ONE_SECOND = 1000; export const ONE_MINUTE = 60 * ONE_SECOND; + +export const DELEGATION_ACTIONS = { + STAKE: "STAKE", + UNBOUND: "UNBOUND", + WITHDRAW_ON_EARLY_UNBOUNDING: "WITHDRAW_ON_EARLY_UNBOUNDING", + WITHDRAW_ON_TIMELOCK: "WITHDRAW_ON_TIMELOCK", + WITHDRAW_ON_TIMELOCK_SLASHING: "WITHDRAW_ON_TIMELOCK_SLASHING", + WITHDRAW_ON_EARLY_UNBOUNDING_SLASHING: + "WITHDRAW_ON_EARLY_UNBOUNDING_SLASHING", +} as const; diff --git a/src/app/hooks/services/useDelegationService.ts b/src/app/hooks/services/useDelegationService.ts new file mode 100644 index 00000000..0e4efd95 --- /dev/null +++ b/src/app/hooks/services/useDelegationService.ts @@ -0,0 +1,202 @@ +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 { useTransactionService } from "./useTransactionService"; + +export type ActionType = keyof typeof ACTIONS; + +interface TxProps { + stakingTxHashHex: string; + stakingTxHex: string; + finalityProviderPk: string; + stakingAmount: number; + paramsVersion: number; + stakingTime: number; + unbondingTxHex: string; + covenantUnbondingSignatures?: { + covenantBtcPkHex: string; + signatureHex: string; + }[]; + state: DelegationV2StakingState; + stakingInput: { + finalityProviderPkNoCoordHex: string; + stakingAmountSat: number; + stakingTimeBlocks: number; + }; + slashingTxHex: string; + unbondingSlashingTxHex: string; +} + +type DelegationCommand = (props: TxProps) => Promise; + +export function useDelegationService() { + const { + delegations = [], + fetchMoreDelegations, + hasMoreDelegations, + isLoading, + findDelegationByTxHash, + } = useDelegationV2State(); + + const { + submitStakingTx, + submitUnbondingTx, + submitEarlyUnbondedWithdrawalTx, + submitTimelockUnbondedWithdrawalTx, + } = useTransactionService(); + + const COMMANDS: Record = useMemo( + () => ({ + [ACTIONS.STAKE]: ({ + stakingInput, + paramsVersion, + stakingTxHashHex, + stakingTxHex, + }: TxProps) => + submitStakingTx( + stakingInput, + paramsVersion, + stakingTxHashHex, + stakingTxHex, + ), + + [ACTIONS.UNBOUND]: async ({ + stakingInput, + paramsVersion, + stakingTxHex, + unbondingTxHex, + covenantUnbondingSignatures, + }: TxProps) => { + if (!covenantUnbondingSignatures) { + throw new Error("Covenant unbonding signatures not found"); + } + + await submitUnbondingTx( + stakingInput, + paramsVersion, + stakingTxHex, + unbondingTxHex, + covenantUnbondingSignatures.map((sig) => ({ + btcPkHex: sig.covenantBtcPkHex, + sigHex: sig.signatureHex, + })), + ); + }, + + [ACTIONS.WITHDRAW_ON_EARLY_UNBOUNDING]: ({ + stakingInput, + paramsVersion, + unbondingTxHex, + }: TxProps) => + submitEarlyUnbondedWithdrawalTx( + stakingInput, + paramsVersion, + unbondingTxHex, + ), + + [ACTIONS.WITHDRAW_ON_TIMELOCK]: async ({ + stakingInput, + paramsVersion, + stakingTxHex, + }: TxProps) => + submitTimelockUnbondedWithdrawalTx( + stakingInput, + paramsVersion, + stakingTxHex, + ), + + [ACTIONS.WITHDRAW_ON_EARLY_UNBOUNDING_SLASHING]: async ({ + stakingInput, + paramsVersion, + unbondingSlashingTxHex, + }) => { + if (!unbondingSlashingTxHex) { + throw new Error( + "Unbonding slashing tx not found, can't submit withdrawal", + ); + } + await submitEarlyUnbondedWithdrawalTx( + stakingInput, + paramsVersion, + unbondingSlashingTxHex, + ); + }, + + [ACTIONS.WITHDRAW_ON_TIMELOCK_SLASHING]: async ({ + stakingInput, + paramsVersion, + slashingTxHex, + }) => { + if (!slashingTxHex) { + throw new Error("Slashing tx not found, can't submit withdrawal"); + } + await submitEarlyUnbondedWithdrawalTx( + stakingInput, + paramsVersion, + slashingTxHex, + ); + }, + }), + [], + ); + + const executeDelegationAction = useCallback( + async (action: string, txHash: string) => { + const d = findDelegationByTxHash(txHash); + + if (!d) { + throw new Error("Delegation not found: " + txHash); + } + + const { + stakingTxHashHex, + stakingTxHex, + finalityProviderBtcPksHex, + stakingAmount, + paramsVersion, + stakingTime, + unbondingTxHex, + covenantUnbondingSignatures, + state, + slashingTxHex, + unbondingSlashingTxHex, + } = d; + + const finalityProviderPk = finalityProviderBtcPksHex[0]; + const stakingInput = { + finalityProviderPkNoCoordHex: finalityProviderPk, + stakingAmountSat: stakingAmount, + stakingTimeBlocks: stakingTime, + }; + + const execute = COMMANDS[action as ActionType]; + + await execute?.({ + stakingTxHashHex, + stakingTxHex, + stakingAmount, + paramsVersion, + stakingTime, + unbondingTxHex, + covenantUnbondingSignatures, + finalityProviderPk, + state, + stakingInput, + slashingTxHex, + unbondingSlashingTxHex, + }); + }, + [COMMANDS, findDelegationByTxHash], + ); + + return { + isLoading, + delegations, + hasMoreDelegations, + fetchMoreDelegations, + executeDelegationAction, + }; +} diff --git a/src/components/delegations/DelegationList/components/ActionButton.tsx b/src/components/delegations/DelegationList/components/ActionButton.tsx index b6896c4e..cada3caf 100644 --- a/src/components/delegations/DelegationList/components/ActionButton.tsx +++ b/src/components/delegations/DelegationList/components/ActionButton.tsx @@ -1,3 +1,4 @@ +import { DELEGATION_ACTIONS as ACTIONS } from "@/app/constants"; import { DelegationV2StakingState as state } from "@/app/types/delegationsV2"; interface ActionButtonProps { @@ -6,50 +7,44 @@ interface ActionButtonProps { onClick?: (action: string, txHash: string) => void; } -type ButtonAdapter = (props: ActionButtonProps) => JSX.Element; -type ButtonStrategy = Record; +const ACTION_BUTTON_PROPS: Record = { + [state.VERIFIED]: { + action: ACTIONS.STAKE, + title: "Stake", + }, + [state.ACTIVE]: { + action: ACTIONS.UNBOUND, + title: "Unbound", + }, + [state.EARLY_UNBONDING_WITHDRAWABLE]: { + action: ACTIONS.WITHDRAW_ON_EARLY_UNBOUNDING, + title: "Withdraw", + }, + [state.TIMELOCK_WITHDRAWABLE]: { + action: ACTIONS.WITHDRAW_ON_TIMELOCK, + title: "Withdraw", + }, + [state.TIMELOCK_SLASHING_WITHDRAWABLE]: { + action: ACTIONS.WITHDRAW_ON_TIMELOCK_SLASHING, + title: "Withdraw", + }, + [state.EARLY_UNBONDING_SLASHING_WITHDRAWABLE]: { + action: ACTIONS.WITHDRAW_ON_EARLY_UNBOUNDING_SLASHING, + title: "Withdraw", + }, +}; -const WithdrawButton = (props: ActionButtonProps) => ( - -); +export function ActionButton(props: ActionButtonProps) { + const buttonProps = ACTION_BUTTON_PROPS[props.state]; -const ACTION_BUTTONS: ButtonStrategy = { - [state.VERIFIED]: (props: ActionButtonProps) => ( - - ), - [state.ACTIVE]: (props: ActionButtonProps) => ( + if (!buttonProps) return null; + + return ( - ), - - [state.EARLY_UNBONDING_WITHDRAWABLE]: WithdrawButton, - [state.TIMELOCK_WITHDRAWABLE]: WithdrawButton, - [state.TIMELOCK_SLASHING_WITHDRAWABLE]: WithdrawButton, - [state.EARLY_UNBONDING_SLASHING_WITHDRAWABLE]: WithdrawButton, -}; - -export function ActionButton(props: ActionButtonProps) { - const Button = ACTION_BUTTONS[props.state]; - if (!Button) { - return null; - } - - return - -
-
-

- Your wallet may contain Bitcoin Ordinals, which are unique digital - assets. If you proceed without filtering, these Ordinals could be - included in future actions involving your balance. -

-

Please select:

-
-
-
- -
-
- -
-
-

- * You can change this setting later if needed -

- -
- - ); -}; diff --git a/src/app/page.tsx b/src/app/page.tsx index 94ea72ce..acffc3b3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,7 +7,6 @@ import { Activity } from "./components/Delegations/Activity"; import { FAQ } from "./components/FAQ/FAQ"; import { Footer } from "./components/Footer/Footer"; import { Header } from "./components/Header/Header"; -import { FilterOrdinalsModal } from "./components/Modals/FilterOrdinalsModal"; import { NetworkBadge } from "./components/NetworkBadge/NetworkBadge"; import { PersonalBalance } from "./components/PersonalBalance/PersonalBalance"; import { Staking } from "./components/Staking/Staking"; @@ -32,7 +31,6 @@ const Home = () => {