From 23819339cf7a372fcfffe8676945cba84472da73 Mon Sep 17 00:00:00 2001 From: Pedro Rezende Date: Wed, 21 Aug 2024 18:41:25 -0300 Subject: [PATCH] refactor: moving withdraw and unbonding to MyValidator correctly --- .../src/App/Staking/StakingOverview.tsx | 23 ++-- .../src/App/Staking/StakingSummary.tsx | 5 +- .../src/App/Staking/UnbondingAmountsTable.tsx | 33 +++-- .../src/App/Staking/UnstakeBondingTable.tsx | 4 +- .../src/App/Staking/WithdrawalButton.tsx | 53 ++++---- apps/namadillo/src/atoms/syncStatus/atoms.ts | 7 +- apps/namadillo/src/atoms/validators/atoms.ts | 109 +++------------- .../src/atoms/validators/functions.ts | 123 ++++++++++-------- .../src/atoms/validators/services.ts | 49 ++----- apps/namadillo/src/types.d.ts | 21 ++- 10 files changed, 182 insertions(+), 245 deletions(-) diff --git a/apps/namadillo/src/App/Staking/StakingOverview.tsx b/apps/namadillo/src/App/Staking/StakingOverview.tsx index 085a3cfd2f..1ca574b802 100644 --- a/apps/namadillo/src/App/Staking/StakingOverview.tsx +++ b/apps/namadillo/src/App/Staking/StakingOverview.tsx @@ -4,12 +4,7 @@ import { PageWithSidebar } from "App/Common/PageWithSidebar"; import { ValidatorDiversification } from "App/Sidebars/ValidatorDiversification"; import { YourStakingDistribution } from "App/Sidebars/YourStakingDistribution"; import { namadaExtensionConnectedAtom } from "atoms/settings"; -import { - myValidatorsAtom, - stakedAmountByAddressAtom, - unbondedAmountByAddressAtom, - withdrawableAmountByAddressAtom, -} from "atoms/validators"; +import { myValidatorsAtom } from "atoms/validators"; import { useAtomValue } from "jotai"; import { AllValidatorsTable } from "./AllValidatorsTable"; import { MyValidatorsTable } from "./MyValidatorsTable"; @@ -25,16 +20,18 @@ import { UnbondingAmountsTable } from "./UnbondingAmountsTable"; export const StakingOverview = (): JSX.Element => { const isConnected = useAtomValue(namadaExtensionConnectedAtom); const myValidators = useAtomValue(myValidatorsAtom); - const unbondedAmounts = useAtomValue(unbondedAmountByAddressAtom); - const withdrawableAmounts = useAtomValue(withdrawableAmountByAddressAtom); - const stakedByAddress = useAtomValue(stakedAmountByAddressAtom); + const hasStaking = - stakedByAddress.isSuccess && Object.keys(stakedByAddress.data).length > 0; + myValidators.isSuccess && + myValidators.data.some((v) => v.stakedAmount?.gt(0)); + const hasUnbonded = - unbondedAmounts.isSuccess && Object.keys(unbondedAmounts.data).length > 0; + myValidators.isSuccess && + myValidators.data.some((v) => v.unbondedAmount?.gt(0)); + const hasWithdraws = - withdrawableAmounts.isSuccess && - Object.keys(withdrawableAmounts.data).length > 0; + myValidators.isSuccess && + myValidators.data.some((v) => v.withdrawableAmount?.gt(0)); return ( diff --git a/apps/namadillo/src/App/Staking/StakingSummary.tsx b/apps/namadillo/src/App/Staking/StakingSummary.tsx index 237af16edb..d905365890 100644 --- a/apps/namadillo/src/App/Staking/StakingSummary.tsx +++ b/apps/namadillo/src/App/Staking/StakingSummary.tsx @@ -40,7 +40,10 @@ export const StakingSummary = (): JSX.Element => { return [ { value: balance, color: "#ffffff" }, { value: totalStaked.totalBonded, color: "#00ffff" }, - { value: totalStaked.totalUnbonded, color: "#DD1599" }, + { + value: totalStaked.totalUnbonded.plus(totalStaked.totalWithdrawable), + color: "#DD1599", + }, ]; }; diff --git a/apps/namadillo/src/App/Staking/UnbondingAmountsTable.tsx b/apps/namadillo/src/App/Staking/UnbondingAmountsTable.tsx index 1c56f51792..14d5f9c842 100644 --- a/apps/namadillo/src/App/Staking/UnbondingAmountsTable.tsx +++ b/apps/namadillo/src/App/Staking/UnbondingAmountsTable.tsx @@ -2,16 +2,17 @@ import { StyledTable, TableRow } from "@namada/components"; import { AtomErrorBoundary } from "App/Common/AtomErrorBoundary"; import { NamCurrency } from "App/Common/NamCurrency"; import { WalletAddress } from "App/Common/WalletAddress"; -import { myUnbondsAtom } from "atoms/validators"; +import { myValidatorsAtom } from "atoms/validators"; import BigNumber from "bignumber.js"; import { useAtomValue } from "jotai"; import { useMemo } from "react"; import { twMerge } from "tailwind-merge"; +import { UnbondingValidator } from "types"; import { ValidatorCard } from "./ValidatorCard"; import { WithdrawalButton } from "./WithdrawalButton"; export const UnbondingAmountsTable = (): JSX.Element => { - const myUnbonds = useAtomValue(myUnbondsAtom); + const myValidators = useAtomValue(myValidatorsAtom); const headers = [ "Validator", "Address", @@ -20,15 +21,14 @@ export const UnbondingAmountsTable = (): JSX.Element => { ]; const rows = useMemo(() => { - if (!myUnbonds.isSuccess) return []; + if (!myValidators.isSuccess) return []; const rowsList: TableRow[] = []; - for (const myValidator of myUnbonds.data) { - const { validator, unbondedAmount, withdrawableAmount } = myValidator; + for (const myValidator of myValidators.data) { + const { validator } = myValidator; + const unbonding = myValidator.withdrawable.concat(myValidator.unbonding); - const amount = new BigNumber(unbondedAmount || withdrawableAmount || 0); - - if (amount.gt(0)) { + unbonding.forEach((entry: UnbondingValidator) => { rowsList.push({ cells: [ { key={`my-validator-currency-${validator.address}`} className="text-right leading-tight" > - + ,
- {myValidator.timeLeft} + {entry.timeLeft}
,
- +
, ], }); - } + }); } return rowsList; - }, [myUnbonds]); + }, [myValidators]); return ( { const change = { validatorId: myValidator.validator.address, - amount: myValidator.withdrawableAmount!, + amount: unbondingStatus.amount, }; const { gasPrice } = useGasEstimate(); @@ -49,29 +51,26 @@ export const WithdrawalButton = ({ }; }, []); - const onWithdraw = useCallback( - async (myValidator: MyValidator) => { - invariant( - account, - "Extension is not connected or you don't have an account" - ); - invariant(gasPrice, "Gas price loading is still pending"); - invariant(gasLimits.isSuccess, "Gas limit loading is still pending"); - invariant( - myValidator.withdrawableAmount, - "Validator doesn't have amounts available for withdrawal" - ); - createWithdrawTx({ - changes: [change], - gasConfig: { - gasPrice: gasPrice!, - gasLimit: gasLimits.data!.Withdraw.native, - }, - account: account!, - }); - }, - [myValidator.withdrawableAmount, gasPrice, gasLimits.isSuccess] - ); + const onWithdraw = useCallback(async () => { + invariant( + account, + "Extension is not connected or you don't have an account" + ); + invariant(gasPrice, "Gas price loading is still pending"); + invariant(gasLimits.isSuccess, "Gas limit loading is still pending"); + invariant( + unbondingStatus.amount, + "Validator doesn't have amounts available for withdrawal" + ); + createWithdrawTx({ + changes: [change], + gasConfig: { + gasPrice: gasPrice!, + gasLimit: gasLimits.data!.Withdraw.native, + }, + account: account!, + }); + }, [unbondingStatus.amount, change, gasPrice, gasLimits.isSuccess]); const dispatchNotification = useSetAtom(dispatchToastNotificationAtom); @@ -133,8 +132,8 @@ export const WithdrawalButton = ({ onWithdraw(myValidator)} + disabled={!unbondingStatus.canWithdraw || isPending || isSuccess} + onClick={() => onWithdraw()} > {isSuccess && "Claimed"} {isPending && "Processing"} diff --git a/apps/namadillo/src/atoms/syncStatus/atoms.ts b/apps/namadillo/src/atoms/syncStatus/atoms.ts index be2fd3c4e3..f50f58ddec 100644 --- a/apps/namadillo/src/atoms/syncStatus/atoms.ts +++ b/apps/namadillo/src/atoms/syncStatus/atoms.ts @@ -1,11 +1,7 @@ import { accountBalanceAtom } from "atoms/accounts/atoms"; import { allProposalsAtom, votedProposalIdsAtom } from "atoms/proposals/atoms"; import { indexerHeartbeatAtom, rpcHeartbeatAtom } from "atoms/settings/atoms"; -import { - allValidatorsAtom, - myUnbondsAtom, - myValidatorsAtom, -} from "atoms/validators/atoms"; +import { allValidatorsAtom, myValidatorsAtom } from "atoms/validators/atoms"; import { atom } from "jotai"; export const syncStatusAtom = atom((get) => { @@ -17,7 +13,6 @@ export const syncStatusAtom = atom((get) => { // Staking get(accountBalanceAtom), get(myValidatorsAtom), - get(myUnbondsAtom), get(allValidatorsAtom), // Governance diff --git a/apps/namadillo/src/atoms/validators/atoms.ts b/apps/namadillo/src/atoms/validators/atoms.ts index f1c8a50cec..6583e9f4c6 100644 --- a/apps/namadillo/src/atoms/validators/atoms.ts +++ b/apps/namadillo/src/atoms/validators/atoms.ts @@ -3,17 +3,13 @@ import { indexerApiAtom } from "atoms/api"; import { chainParametersAtom } from "atoms/chain"; import { shouldUpdateBalanceAtom } from "atoms/etc"; import { queryDependentFn } from "atoms/utils"; -import BigNumber from "bignumber.js"; -import { - AtomWithQueryResult, - UndefinedInitialDataOptions, - atomWithQuery, -} from "jotai-tanstack-query"; -import { MyUnbondingValidator, MyValidator, Validator } from "types"; +import { atomWithQuery } from "jotai-tanstack-query"; +import { MyValidator, Validator } from "types"; +import { toMyValidators } from "./functions"; import { fetchAllValidators, - fetchMyUnbonds, - fetchMyValidators, + fetchMyBondedAmounts, + fetchMyUnbondedAmounts, fetchVotingPower, } from "./services"; @@ -51,85 +47,20 @@ export const myValidatorsAtom = atomWithQuery((get) => { return { queryKey: ["my-validators", account.data?.address], refetchInterval: enablePolling ? 1000 : false, - ...queryDependentFn( - async (): Promise => - fetchMyValidators( - api, - account.data!, - chainParameters.data!, - votingPower.data! - ), - [account, chainParameters, votingPower] - ), + ...queryDependentFn(async (): Promise => { + const bondedAmountsQuery = fetchMyBondedAmounts(api, account.data!); + const unbondedAmountsQuery = fetchMyUnbondedAmounts(api, account.data!); + const [unbondedAmounts, bondedAmounts] = await Promise.all([ + unbondedAmountsQuery, + bondedAmountsQuery, + ]); + return toMyValidators( + bondedAmounts, + unbondedAmounts, + votingPower.data!, + chainParameters.data!.epochInfo, + chainParameters.data!.apr + ); + }, [account, chainParameters, votingPower]), }; }); - -export const myUnbondsAtom = atomWithQuery((get) => { - const chainParameters = get(chainParametersAtom); - const account = get(defaultAccountAtom); - const votingPower = get(votingPowerAtom); - const api = get(indexerApiAtom); - - // TODO: Refactor after this event subscription is enabled in the indexer - const enablePolling = get(shouldUpdateBalanceAtom); - return { - queryKey: ["my-unbonds", account.data?.address], - refetchInterval: enablePolling ? 1000 : false, - ...queryDependentFn( - async (): Promise => - fetchMyUnbonds( - api, - account.data!, - chainParameters.data!, - votingPower.data! - ), - [account, chainParameters, votingPower] - ), - }; -}); - -export const unbondedAmountByAddressAtom = atomWithQuery((get) => - deriveFromMyValidatorsAtom( - "unbonded-amount", - "unbondedAmount", - get(myUnbondsAtom) - ) -); - -export const withdrawableAmountByAddressAtom = atomWithQuery((get) => - deriveFromMyValidatorsAtom( - "withdrawable-amount", - "withdrawableAmount", - get(myUnbondsAtom) - ) -); - -export const stakedAmountByAddressAtom = atomWithQuery((get) => - deriveFromMyValidatorsAtom( - "staked-amount", - "stakedAmount", - get(myValidatorsAtom) - ) -); - -const deriveFromMyValidatorsAtom = ( - key: string, - property: "stakedAmount" | "unbondedAmount" | "withdrawableAmount", - myValidators: AtomWithQueryResult< - (MyValidator | MyUnbondingValidator)[], - Error - > -): UndefinedInitialDataOptions> => { - return { - queryKey: [key, myValidators.data], - enabled: myValidators.isSuccess, - queryFn: async () => { - return myValidators.data!.reduce((prev, current) => { - if (current[property]?.gt(0)) { - return { ...prev, [current.validator.address]: current[property] }; - } - return prev; - }, {}); - }, - }; -}; diff --git a/apps/namadillo/src/atoms/validators/functions.ts b/apps/namadillo/src/atoms/validators/functions.ts index b3974d086a..dccfe5c3d0 100644 --- a/apps/namadillo/src/atoms/validators/functions.ts +++ b/apps/namadillo/src/atoms/validators/functions.ts @@ -6,7 +6,13 @@ import { } from "@anomaorg/namada-indexer-client"; import { singleUnitDurationFromInterval } from "@namada/utils"; import BigNumber from "bignumber.js"; -import { EpochInfo, MyUnbondingValidator, MyValidator, Validator } from "types"; +import { + Address, + EpochInfo, + MyValidator, + UnbondingValidator, + Validator, +} from "types"; export const toValidator = ( indexerValidator: IndexerValidator, @@ -45,67 +51,82 @@ export const toValidator = ( }; }; +export const calculateUnbondingTimeLeft = (unbond: IndexerUnbond): string => { + const timeNow = Math.round(Date.now() / 1000); + const withdrawTime = Number(unbond.withdrawTime); + const canWithdraw = unbond.canWithdraw; + const timeLeft = + canWithdraw ? "" + // If can't withdraw but estimation is incorrect display withdraw epoch + : withdrawTime < timeNow ? `Epoch ${unbond.withdrawEpoch}` + : singleUnitDurationFromInterval(timeNow, withdrawTime); + return timeLeft; +}; + export const toMyValidators = ( indexerBonds: IndexerBond[], + indexerUnbonds: IndexerUnbond[], totalVotingPower: IndexerVotingPower, epochInfo: EpochInfo, apr: BigNumber ): MyValidator[] => { - return indexerBonds.map((indexerBond) => { - const validator = toValidator( - indexerBond.validator, - totalVotingPower, - epochInfo, - apr - ); + const myValidators: Record = {}; - return { - uuid: String(indexerBond.validator.validatorId), - stakingStatus: "bonded", - stakedAmount: BigNumber(indexerBond.amount), - unbondedAmount: BigNumber(0), - withdrawableAmount: BigNumber(0), - validator, - }; - }); -}; + const createEntryIfDoesntExist = (validator: IndexerValidator): void => { + if (!myValidators.hasOwnProperty(validator.address)) { + myValidators[validator.address] = { + withdrawableAmount: new BigNumber(0), + stakedAmount: new BigNumber(0), + unbondedAmount: new BigNumber(0), + bonds: [], + unbonding: [], + withdrawable: [], + validator: toValidator(validator, totalVotingPower, epochInfo, apr), + }; + } + }; -export const toUnbondingValidators = ( - indexerBonds: IndexerUnbond[], - totalVotingPower: IndexerVotingPower, - epochInfo: EpochInfo, - apr: BigNumber -): MyUnbondingValidator[] => { - const timeNow = Math.round(Date.now() / 1000); + const addBondToAddress = ( + address: Address, + key: "bonds" | "unbonding" | "withdrawable", + bond: IndexerBond | IndexerUnbond + ): void => { + const { validator: _, ...bondsWithoutValidator } = bond; + myValidators[address]![key].push(bondsWithoutValidator); + }; - return indexerBonds.map((indexerUnbond) => { - const validator = toValidator( - indexerUnbond.validator, - totalVotingPower, - epochInfo, - apr - ); - const withdrawTime = Number(indexerUnbond.withdrawTime); + const incrementAmount = ( + address: Address, + prop: keyof Pick< + MyValidator, + "stakedAmount" | "withdrawableAmount" | "unbondedAmount" + >, + amount: BigNumber | string + ): void => { + myValidators[address][prop] = myValidators[address][prop]!.plus(amount); + }; - const canWithdraw = indexerUnbond.canWithdraw; - const timeLeft = - canWithdraw ? "" - // If can't withdraw but estimation is incorrect display withdraw epoch - : withdrawTime < timeNow ? `Epoch ${indexerUnbond.withdrawEpoch}` - : singleUnitDurationFromInterval(timeNow, withdrawTime); + for (const bond of indexerBonds) { + const { address } = bond.validator; + createEntryIfDoesntExist(bond.validator); + incrementAmount(address, "stakedAmount", bond.amount); + addBondToAddress(address, "bonds", { ...bond }); + } - const amountValue = BigNumber(indexerUnbond.amount); - const amount = { - [canWithdraw ? "withdrawableAmount" : "unbondedAmount"]: amountValue, + for (const unbond of indexerUnbonds) { + const { address } = unbond.validator; + const unbondingDetails: UnbondingValidator = { + ...unbond, + timeLeft: calculateUnbondingTimeLeft(unbond), }; + if (unbond.canWithdraw) { + addBondToAddress(address, "withdrawable", unbondingDetails); + incrementAmount(address, "withdrawableAmount", unbond.amount); + } else { + addBondToAddress(address, "unbonding", unbondingDetails); + incrementAmount(address, "unbondedAmount", unbond.amount); + } + } - return { - uuid: String(indexerUnbond.validator.validatorId), - stakingStatus: "unbonded", - stakedAmount: BigNumber(0), - timeLeft, - validator, - ...amount, - }; - }); + return Object.values(myValidators); }; diff --git a/apps/namadillo/src/atoms/validators/services.ts b/apps/namadillo/src/atoms/validators/services.ts index 7ae8dd3dbe..842001c3b4 100644 --- a/apps/namadillo/src/atoms/validators/services.ts +++ b/apps/namadillo/src/atoms/validators/services.ts @@ -2,20 +2,13 @@ import { DefaultApi, ValidatorStatus as IndexerValidatorStatus, VotingPower as IndexerVotingPower, + MergedBond, + Unbond, VotingPower, } from "@anomaorg/namada-indexer-client"; import { Account } from "@namada/types"; -import { - ChainParameters, - MyUnbondingValidator, - MyValidator, - Validator, -} from "types"; -import { - toMyValidators, - toUnbondingValidators, - toValidator, -} from "./functions"; +import { ChainParameters, Validator } from "types"; +import { toValidator } from "./functions"; export const fetchVotingPower = async ( api: DefaultApi @@ -41,40 +34,22 @@ export const fetchAllValidators = async ( ); }; -export const fetchMyValidators = async ( +export const fetchMyBondedAmounts = async ( api: DefaultApi, - account: Account, - chainParameters: ChainParameters, - votingPower: IndexerVotingPower -): Promise => { - const epochInfo = chainParameters.epochInfo; - const apr = chainParameters.apr; + account: Account +): Promise => { const bondsResponse = await api.apiV1PosMergedBondsAddressGet( account.address ); - return toMyValidators( - bondsResponse.data.results, - votingPower, - epochInfo, - apr - ); + return bondsResponse.data.results; }; -export const fetchMyUnbonds = async ( +export const fetchMyUnbondedAmounts = async ( api: DefaultApi, - account: Account, - chainParameters: ChainParameters, - votingPower: IndexerVotingPower -): Promise => { - const epochInfo = chainParameters.epochInfo; - const apr = chainParameters.apr; + account: Account +): Promise => { const unbondsResponse = await api.apiV1PosMergedUnbondsAddressGet( account.address ); - return toUnbondingValidators( - unbondsResponse.data.results, - votingPower, - epochInfo, - apr - ); + return unbondsResponse.data.results; }; diff --git a/apps/namadillo/src/types.d.ts b/apps/namadillo/src/types.d.ts index 0c7bde1de4..9833e5f9f1 100644 --- a/apps/namadillo/src/types.d.ts +++ b/apps/namadillo/src/types.d.ts @@ -1,3 +1,7 @@ +import { + Bond as IndexerBond, + Unbond as IndexerUnbond, +} from "@anomaorg/namada-indexer-client"; import { ChainKey, ExtensionKey } from "@namada/types"; import BigNumber from "bignumber.js"; @@ -75,16 +79,23 @@ export type Validator = Unique & { imageUrl?: string; }; +export type UnbondingValidator = Omit< + | (IndexerUnbond & { + timeLeft: string; + }) + | "validator" +>; + +export type BondingValidator = Omit; + export type MyValidator = { - stakingStatus: string; stakedAmount?: BigNumber; unbondedAmount?: BigNumber; withdrawableAmount?: BigNumber; validator: Validator; -}; - -export type MyUnbondingValidator = MyValidator & { - timeLeft: string; + bonds: BondingValidator[]; + unbonding: UnbondingValidator[]; + withdrawable: UnbondingValidator[]; }; export type StakingTotals = {