diff --git a/apps/wallet/src/ui/app/helpers/index.ts b/apps/wallet/src/ui/app/helpers/index.ts index 7b626f94293b5..e364f487e335c 100644 --- a/apps/wallet/src/ui/app/helpers/index.ts +++ b/apps/wallet/src/ui/app/helpers/index.ts @@ -6,3 +6,4 @@ export { default as notEmpty } from './notEmptyCheck'; export { parseAmount } from './parseAmount'; export { getEventsSummary } from './getEventsSummary'; export { getAmount } from './getAmount'; +export { roundFloat } from './roundFloat'; diff --git a/apps/wallet/src/ui/app/helpers/roundFloat.ts b/apps/wallet/src/ui/app/helpers/roundFloat.ts new file mode 100644 index 0000000000000..4a743b377292f --- /dev/null +++ b/apps/wallet/src/ui/app/helpers/roundFloat.ts @@ -0,0 +1,6 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +export function roundFloat(num: number, precision = 4) { + return parseFloat(num.toFixed(precision)); +} diff --git a/apps/wallet/src/ui/app/pages/home/tokens/TokenIconLink.tsx b/apps/wallet/src/ui/app/pages/home/tokens/TokenIconLink.tsx new file mode 100644 index 0000000000000..ca095307e200f --- /dev/null +++ b/apps/wallet/src/ui/app/pages/home/tokens/TokenIconLink.tsx @@ -0,0 +1,99 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { useFeature } from '@growthbook/growthbook-react'; +import { SUI_TYPE_ARG, type SuiAddress } from '@mysten/sui.js'; +import cl from 'classnames'; +import { useMemo } from 'react'; +import { Link } from 'react-router-dom'; + +import { DelegatedAPY } from '_app/shared/delegated-apy'; +import { Text } from '_app/shared/text'; +import { useGetDelegatedStake } from '_app/staking/useGetDelegatedStake'; +import Icon from '_components/icon'; +import LoadingIndicator from '_components/loading/LoadingIndicator'; +import { SuiIcons } from '_font-icons/output/sui-icons'; +import { useFormatCoin } from '_hooks'; +import { FEATURES } from '_src/shared/experimentation/features'; + +export function TokenIconLink({ + accountAddress, +}: { + accountAddress: SuiAddress; +}) { + const stakingEnabled = useFeature(FEATURES.STAKING_ENABLED).on; + const { data: delegations, isLoading } = + useGetDelegatedStake(accountAddress); + + const totalActivePendingStake = useMemo(() => { + if (!delegations) return 0n; + return delegations.reduce( + (acc, { staked_sui }) => acc + BigInt(staked_sui.principal.value), + 0n + ); + }, [delegations]); + + const stakedValidators = useMemo(() => { + if (!delegations) return []; + return delegations.map( + ({ staked_sui }) => staked_sui.validator_address + ); + }, [delegations]); + + const [formatted, symbol, queryResult] = useFormatCoin( + totalActivePendingStake, + SUI_TYPE_ARG + ); + + return ( + + {isLoading || queryResult.isLoading ? ( +
+ +
+ ) : ( +
+ +
+ + {totalActivePendingStake + ? 'Currently Staked' + : 'Stake & Earn SUI'} + + {!!totalActivePendingStake && ( + + {formatted} {symbol} + + )} +
+
+ )} +
+ {stakingEnabled && ( + + )} +
+ + ); +} diff --git a/apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx b/apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx index e3eaec7177e6e..975f24e2ea1d3 100644 --- a/apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx +++ b/apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx @@ -1,10 +1,10 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { useFeature } from '@growthbook/growthbook-react'; import cl from 'classnames'; import { useMemo } from 'react'; +import { TokenIconLink } from './TokenIconLink'; import CoinBalance from './coin-balance'; import IconLink from './icon-link'; import FaucetRequestButton from '_app/shared/faucet/request-button'; @@ -17,7 +17,6 @@ import { SuiIcons } from '_font-icons/output/sui-icons'; import { useAppSelector, useObjectsState } from '_hooks'; import { accountAggregateBalancesSelector } from '_redux/slices/account'; import { GAS_TYPE_ARG, Coin } from '_redux/slices/sui-objects/Coin'; -import { FEATURES } from '_src/shared/experimentation/features'; import st from './TokensPage.module.scss'; @@ -82,6 +81,7 @@ function MyTokens({ function TokenDetails({ coinType }: TokenDetailsProps) { const { loading, error, showError } = useObjectsState(); const activeCoinType = coinType || GAS_TYPE_ARG; + const accountAddress = useAppSelector(({ account }) => account.address); const balances = useAppSelector(accountAggregateBalancesSelector); const tokenBalance = balances[activeCoinType] || BigInt(0); const allCoinTypes = useMemo(() => Object.keys(balances), [balances]); @@ -93,8 +93,6 @@ function TokenDetails({ coinType }: TokenDetailsProps) { [activeCoinType] ); - const stakingEnabled = useFeature(FEATURES.STAKING_ENABLED).on; - return ( <> {coinType && ( @@ -151,15 +149,8 @@ function TokenDetails({ coinType }: TokenDetailsProps) { /> - {activeCoinType === GAS_TYPE_ARG ? ( -
- -
+ {activeCoinType === GAS_TYPE_ARG && accountAddress ? ( + ) : null} {!coinType ? ( diff --git a/apps/wallet/src/ui/app/pages/home/tokens/icon-link/IconLink.module.scss b/apps/wallet/src/ui/app/pages/home/tokens/icon-link/IconLink.module.scss index 51390495ea525..161240ba9c245 100644 --- a/apps/wallet/src/ui/app/pages/home/tokens/icon-link/IconLink.module.scss +++ b/apps/wallet/src/ui/app/pages/home/tokens/icon-link/IconLink.module.scss @@ -7,7 +7,7 @@ flex-flow: column nowrap; align-items: center; background-color: color.adjust(colors.$sui-blue, $alpha: -0.9); - border-radius: 10px; + border-radius: 15px; padding: 10px 14px; text-decoration: none; color: colors.$cta-blue; diff --git a/apps/wallet/src/ui/app/shared/card/CardItem.tsx b/apps/wallet/src/ui/app/shared/card/CardItem.tsx index 348df11237502..3a22845511905 100644 --- a/apps/wallet/src/ui/app/shared/card/CardItem.tsx +++ b/apps/wallet/src/ui/app/shared/card/CardItem.tsx @@ -14,14 +14,14 @@ export function CardItem({ title, children }: CardItemProps) { return (
{title} -
{children}
+ {children}
); } diff --git a/apps/wallet/src/ui/app/shared/delegated-apy/index.tsx b/apps/wallet/src/ui/app/shared/delegated-apy/index.tsx new file mode 100644 index 0000000000000..6f23fba791276 --- /dev/null +++ b/apps/wallet/src/ui/app/shared/delegated-apy/index.tsx @@ -0,0 +1,84 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { type SuiAddress } from '@mysten/sui.js'; +import { useMemo } from 'react'; + +import { calculateAPY } from '../../staking/calculateAPY'; +import { STATE_OBJECT } from '../../staking/usePendingDelegation'; +import { Text } from '_app/shared/text'; +import { IconTooltip } from '_app/shared/tooltip'; +import { validatorsFields } from '_app/staking/validatorsFields'; +import LoadingIndicator from '_components/loading/LoadingIndicator'; +import { roundFloat } from '_helpers'; +import { useGetObject } from '_hooks'; + +const APY_DECIMALS = 3; + +type DelegatedAPYProps = { + stakedValidators: SuiAddress[]; +}; + +export function DelegatedAPY({ stakedValidators }: DelegatedAPYProps) { + const { data, isLoading } = useGetObject(STATE_OBJECT); + + const validatorsData = data && validatorsFields(data); + + const averageNetworkAPY = useMemo(() => { + if (!validatorsData) return 0; + const validators = validatorsData.validators.fields.active_validators; + + let stakedAPYs = 0; + + validators.forEach((validator) => { + if ( + stakedValidators.includes( + validator.fields.delegation_staking_pool.fields + .validator_address + ) + ) { + stakedAPYs += calculateAPY(validator, +validatorsData.epoch); + } + }); + + const averageAPY = stakedAPYs / stakedValidators.length; + + return roundFloat(averageAPY || 0, APY_DECIMALS); + }, [stakedValidators, validatorsData]); + + if (isLoading) { + return ( +
+ +
+ ); + } + return ( +
+ {averageNetworkAPY > 0 ? ( + <> + + {averageNetworkAPY} + + + % APY + +
+ +
+ + ) : ( + + -- + + )} +
+ ); +} diff --git a/apps/wallet/src/ui/app/shared/heading/index.tsx b/apps/wallet/src/ui/app/shared/heading/index.tsx index ce61b92afa018..69cf072baa840 100644 --- a/apps/wallet/src/ui/app/shared/heading/index.tsx +++ b/apps/wallet/src/ui/app/shared/heading/index.tsx @@ -30,12 +30,14 @@ const headingStyles = cva( 'gray-75': 'text-gray-75', 'gray-70': 'text-gray-70', 'gray-65': 'text-gray-65', + 'gray-60': 'text-gray-60', 'sui-dark': 'text-sui-dark', sui: 'text-sui', 'sui-light': 'text-sui-light', steel: 'text-steel', 'steel-dark': 'text-steel-dark', 'steel-darker': 'text-steel-darker', + 'success-dark': 'text-success-dark', }, weight: { medium: 'font-medium', diff --git a/apps/wallet/src/ui/app/shared/tooltip/index.tsx b/apps/wallet/src/ui/app/shared/tooltip/index.tsx index 16c52ea52b728..ae52a02a6f5b6 100644 --- a/apps/wallet/src/ui/app/shared/tooltip/index.tsx +++ b/apps/wallet/src/ui/app/shared/tooltip/index.tsx @@ -106,7 +106,7 @@ export function Tooltip({ tip, children, placement = 'top' }: TooltipProps) { {open ? ( diff --git a/apps/wallet/src/ui/app/staking/calculateAPY.ts b/apps/wallet/src/ui/app/staking/calculateAPY.ts index 2a2120a306c85..26726ce30191c 100644 --- a/apps/wallet/src/ui/app/staking/calculateAPY.ts +++ b/apps/wallet/src/ui/app/staking/calculateAPY.ts @@ -3,6 +3,8 @@ import { type ActiveValidator } from '@mysten/sui.js'; +import { roundFloat } from '_helpers'; + const APY_DECIMALS = 4; export function calculateAPY(validators: ActiveValidator, epoch: number) { @@ -16,5 +18,5 @@ export function calculateAPY(validators: ActiveValidator, epoch: number) { +delegation_token_supply.fields.value, 365 / num_epochs_participated - 1 ); - return apy ? parseFloat(apy.toFixed(APY_DECIMALS)) : 0; + return apy ? roundFloat(apy, APY_DECIMALS) : 0; } diff --git a/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx b/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx index 85e6ea61facec..9e91496f27adf 100644 --- a/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx +++ b/apps/wallet/src/ui/app/staking/delegation-detail/DelegationDetailCard.tsx @@ -2,13 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 import { useFeature } from '@growthbook/growthbook-react'; -import { is, SuiObject, type ValidatorsFields } from '@mysten/sui.js'; import { useMemo } from 'react'; import { calculateAPY } from '../calculateAPY'; +import { getStakingRewards } from '../getStakingRewards'; import { StakeAmount } from '../home/StakeAmount'; import { useGetDelegatedStake } from '../useGetDelegatedStake'; import { STATE_OBJECT } from '../usePendingDelegation'; +import { validatorsFields } from '../validatorsFields'; import BottomMenuLayout, { Content } from '_app/shared/bottom-menu-layout'; import Button from '_app/shared/button'; import { Card } from '_app/shared/card'; @@ -31,7 +32,7 @@ export function DelegationDetailCard({ stakedId, }: DelegationDetailCardProps) { const { - data: validatetors, + data: validators, isLoading: loadingValidators, isError: errorValidators, } = useGetObject(STATE_OBJECT); @@ -44,12 +45,7 @@ export function DelegationDetailCard({ isError, } = useGetDelegatedStake(accountAddress || ''); - const validatorsData = - validatetors && - is(validatetors.details, SuiObject) && - validatetors.details.data.dataType === 'moveObject' - ? (validatetors.details.data.fields as ValidatorsFields) - : null; + const validatorsData = validatorsFields(validators); const validatorData = useMemo(() => { if (!validatorsData) return null; @@ -68,17 +64,14 @@ export function DelegationDetailCard({ const totalStake = delegationData?.staked_sui.principal.value || 0n; + // Stake earned by ratio * pending_reward const suiEarned = useMemo(() => { - if ( - !delegationData || - typeof delegationData.delegation_status !== 'object' - ) - return 0n; - return BigInt( - delegationData.delegation_status.Active.pool_tokens.value - - delegationData.delegation_status.Active.principal_sui_amount + if (!validatorsData || !delegationData) return 0n; + return getStakingRewards( + validatorsData.validators.fields.active_validators, + delegationData ); - }, [delegationData]); + }, [delegationData, validatorsData]); const apy = useMemo(() => { if (!validatorData || !validatorsData) return 0; @@ -96,6 +89,11 @@ export function DelegationDetailCard({ staked: stakedId, }).toString()}`; + const commission = useMemo(() => { + if (!validatorData) return 0; + return +validatorData.fields.commission_rate * 100; + }, [validatorData]); + const stakingEnabled = useFeature(FEATURES.STAKING_ENABLED).on; if (isLoading || loadingValidators) { @@ -130,14 +128,14 @@ export function DelegationDetailCard({ @@ -197,10 +195,7 @@ export function DelegationDetailCard({ weight="semibold" color="gray-90" > - { - validatorData?.fields - .commission_rate - } + {commission} + fields.delegation_staking_pool.fields.validator_address === + validatorAddress + ); + + if (!validator) return 0; + const { fields: validatorFields } = validator; + + const poolTokens = new BigNumber( + delegation.delegation_status.Active.pool_tokens.value + ); + const delegationTokenSupply = new BigNumber( + validatorFields.delegation_staking_pool.fields.delegation_token_supply.fields.value + ); + const suiBalance = new BigNumber( + validatorFields.delegation_staking_pool.fields.sui_balance + ); + const pricipalAmout = new BigNumber( + delegation.delegation_status.Active.principal_sui_amount + ); + const currentSuiWorth = poolTokens + .multipliedBy(suiBalance) + .dividedBy(delegationTokenSupply); + + const earnToken = currentSuiWorth.decimalPlaces(0, 1).minus(pricipalAmout); + return earnToken.toNumber(); +} diff --git a/apps/wallet/src/ui/app/staking/home/DelegationCard.tsx b/apps/wallet/src/ui/app/staking/home/DelegationCard.tsx index 5cbebc2093e11..a80e4bd7926ec 100644 --- a/apps/wallet/src/ui/app/staking/home/DelegationCard.tsx +++ b/apps/wallet/src/ui/app/staking/home/DelegationCard.tsx @@ -1,14 +1,18 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +import { SUI_TYPE_ARG } from '@mysten/sui.js'; +import { useMemo } from 'react'; import { Link } from 'react-router-dom'; -import { GAS_TYPE_ARG } from '../../redux/slices/sui-objects/Coin'; +import { getStakingRewards } from '../getStakingRewards'; import { ValidatorLogo } from '../validators/ValidatorLogo'; import { useFormatCoin } from '_app/hooks'; import { Text } from '_src/ui/app/shared/text'; import { IconTooltip } from '_src/ui/app/shared/tooltip'; +import type { ActiveValidator, DelegatedStake } from '@mysten/sui.js'; + export enum DelegationState { WARM_UP = 'WARM_UP', EARNING = 'EARNING', @@ -16,11 +20,9 @@ export enum DelegationState { } interface DelegationCardProps { - staked: number | bigint; - state: DelegationState; - rewards?: number | bigint; - address: string; - stakedId: string; + delegationObject: DelegatedStake; + activeValidators: ActiveValidator[]; + currentEpoch: number; } export const STATE_TO_COPY = { @@ -28,18 +30,28 @@ export const STATE_TO_COPY = { [DelegationState.EARNING]: 'Staking Reward', [DelegationState.COOL_DOWN]: 'In Cool-down', }; - -// TODO: Add these classes when we add delegation detail page. - +// For delegationsRequestEpoch n through n + 2, show Start Earning +// For delegationsRequestEpoch n + 3, show Staking Reward +// Show epoch number or date/time for n + 3 epochs +// TODO: Add cool-down state export function DelegationCard({ - staked, - rewards, - state, - address, - stakedId, + delegationObject, + activeValidators, + currentEpoch, }: DelegationCardProps) { - const [stakedFormatted] = useFormatCoin(staked, GAS_TYPE_ARG); - const [rewardsFormatted] = useFormatCoin(rewards, GAS_TYPE_ARG); + const { staked_sui } = delegationObject; + const address = staked_sui.validator_address; + const staked = staked_sui.principal.value; + const rewards = useMemo( + () => getStakingRewards(activeValidators, delegationObject), + [activeValidators, delegationObject] + ); + + const stakedId = staked_sui.id.id; + const delegationsRequestEpoch = staked_sui.delegation_request_epoch; + const numberOfEpochPastRequesting = currentEpoch - delegationsRequestEpoch; + const [stakedFormatted] = useFormatCoin(staked, SUI_TYPE_ARG); + const [rewardsFormatted] = useFormatCoin(rewards, SUI_TYPE_ARG); return ( -
+
-
-
+
+
{stakedFormatted} @@ -76,12 +88,24 @@ export function DelegationCard({
-
+
- {STATE_TO_COPY[state]} + {numberOfEpochPastRequesting > 2 + ? 'Staking Reward' + : 'Starts Earning'} - {!!rewards && ( -
+ {numberOfEpochPastRequesting <= 2 && ( + + Epoch #{delegationsRequestEpoch + 2} + + )} + + {rewards > 0 && numberOfEpochPastRequesting > 2 && ( +
{rewardsFormatted} SUI
)} diff --git a/apps/wallet/src/ui/app/staking/home/StakeAmount.tsx b/apps/wallet/src/ui/app/staking/home/StakeAmount.tsx index 134865bfb942a..b58c2151017b9 100644 --- a/apps/wallet/src/ui/app/staking/home/StakeAmount.tsx +++ b/apps/wallet/src/ui/app/staking/home/StakeAmount.tsx @@ -3,13 +3,14 @@ import { SUI_TYPE_ARG } from '@mysten/sui.js'; +import { Heading } from '_app/shared/heading'; import { Text } from '_app/shared/text'; import { useFormatCoin } from '_hooks'; //TODO unify StakeAmount and CoinBalance interface StakeAmountProps { balance: bigint | number | string; - variant: 'heading4' | 'body'; + variant: 'heading5' | 'body'; isEarnedRewards?: boolean; } @@ -23,20 +24,32 @@ export function StakeAmount({ const zeroBalanceColor = !!balance; const earnRewardColor = isEarnedRewards && (zeroBalanceColor ? 'success-dark' : 'gray-60'); - const colorAmount = variant === 'heading4' ? 'gray-90' : 'steel-darker'; - const colorSymbol = variant === 'heading4' ? 'steel' : 'steel-darker'; + const colorAmount = variant === 'heading5' ? 'gray-90' : 'steel-darker'; + const colorSymbol = variant === 'heading5' ? 'steel' : 'steel-darker'; return (
+ {variant === 'heading5' ? ( + + {formatted} + + ) : ( + + {formatted} + + )} + - {formatted} - - diff --git a/apps/wallet/src/ui/app/staking/stake/StakeForm.tsx b/apps/wallet/src/ui/app/staking/stake/StakeForm.tsx index 20f8fa6ac8384..461862ffe344e 100644 --- a/apps/wallet/src/ui/app/staking/stake/StakeForm.tsx +++ b/apps/wallet/src/ui/app/staking/stake/StakeForm.tsx @@ -141,7 +141,10 @@ function StakeForm({ weight="medium" color="steel-darker" > - {unstake ? 0 : calculateRemaining} {symbol} + {calculateRemaining <= 0 + ? 0 + : calculateRemaining}{' '} + {symbol}
)} diff --git a/apps/wallet/src/ui/app/staking/stake/ValidatorFormDetail.tsx b/apps/wallet/src/ui/app/staking/stake/ValidatorFormDetail.tsx index 9c941e2223b6a..16b19a9ecf738 100644 --- a/apps/wallet/src/ui/app/staking/stake/ValidatorFormDetail.tsx +++ b/apps/wallet/src/ui/app/staking/stake/ValidatorFormDetail.tsx @@ -1,6 +1,6 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { is, SuiObject, type ValidatorsFields } from '@mysten/sui.js'; + import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; @@ -9,6 +9,7 @@ import { StakeAmount } from '../home/StakeAmount'; import { useGetDelegatedStake } from '../useGetDelegatedStake'; import { STATE_OBJECT } from '../usePendingDelegation'; import { ValidatorLogo } from '../validators/ValidatorLogo'; +import { validatorsFields } from '../validatorsFields'; import { Card } from '_app/shared/card'; import Alert from '_components/alert'; import LoadingIndicator from '_components/loading/LoadingIndicator'; @@ -32,7 +33,7 @@ export function ValidatorFormDetail({ const [searchParams] = useSearchParams(); const stakeIdParams = searchParams.get('staked'); const { - data: validatetors, + data: validators, isLoading: loadingValidators, isError: errorValidators, } = useGetObject(STATE_OBJECT); @@ -44,22 +45,7 @@ export function ValidatorFormDetail({ error, } = useGetDelegatedStake(accountAddress || ''); - const validatorsData = - validatetors && - is(validatetors.details, SuiObject) && - validatetors.details.data.dataType === 'moveObject' - ? (validatetors.details.data.fields as ValidatorsFields) - : null; - - const delegationData = useMemo(() => { - if (!allDelegation) return null; - - return allDelegation.find( - ({ staked_sui }) => staked_sui.id.id === stakedId - ); - }, [allDelegation, stakedId]); - - const totalSuiStake = delegationData?.staked_sui.principal.value || 0n; + const validatorsData = validatorsFields(validators); const validatorData = useMemo(() => { if (!validatorsData) return null; @@ -68,7 +54,8 @@ export function ValidatorFormDetail({ ); }, [validatorAddress, validatorsData]); - const totalValidatorStake = validatorData?.fields.stake_amount || 0; + const totalValidatorStake = + validatorData?.fields.delegation_staking_pool.fields.sui_balance || 0; const totalStake = useMemo(() => { if (!allDelegation) return 0n; @@ -174,13 +161,10 @@ export function ValidatorFormDetail({ > Total Staked +
diff --git a/apps/wallet/src/ui/app/staking/validators/SelectValidatorCard.tsx b/apps/wallet/src/ui/app/staking/validators/SelectValidatorCard.tsx index c3042374ff9de..a8f9e2d0871d3 100644 --- a/apps/wallet/src/ui/app/staking/validators/SelectValidatorCard.tsx +++ b/apps/wallet/src/ui/app/staking/validators/SelectValidatorCard.tsx @@ -1,7 +1,6 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { is, SuiObject, type ValidatorsFields } from '@mysten/sui.js'; import cl from 'classnames'; import { useState, useMemo } from 'react'; @@ -11,6 +10,7 @@ import { ValidatorListItem } from './ValidatorListItem'; import { Content, Menu } from '_app/shared/bottom-menu-layout'; import Button from '_app/shared/button'; import { Text } from '_app/shared/text'; +import { validatorsFields } from '_app/staking/validatorsFields'; import Alert from '_components/alert'; import Icon, { SuiIcons } from '_components/icon'; import LoadingIndicator from '_components/loading/LoadingIndicator'; @@ -21,16 +21,11 @@ export function SelectValidatorCard() { null ); const [sortKey, setSortKey] = useState<'name' | 'apy'>('apy'); - const [sortAscending, setSortAscending] = useState(true); + const [sortAscending, setSortAscending] = useState(false); const { data, isLoading, isError } = useGetObject(STATE_OBJECT); - const validatorsData = - data && - is(data.details, SuiObject) && - data.details.data.dataType === 'moveObject' - ? (data.details.data.fields as ValidatorsFields) - : null; + const validatorsData = data && validatorsFields(data); const selectValidator = (address: string) => { setSelectedValidator((state) => (state !== address ? address : null)); @@ -118,8 +113,8 @@ export function SelectValidatorCard() { className={cl( 'text-captionSmall font-thin text-hero', sortAscending - ? '-rotate-90' - : 'rotate-90' + ? 'rotate-90' + : '-rotate-90' )} /> )} @@ -146,8 +141,8 @@ export function SelectValidatorCard() { className={cl( 'text-captionSmall font-thin text-hero', sortAscending - ? '-rotate-90' - : 'rotate-90' + ? 'rotate-90' + : '-rotate-90' )} /> )} diff --git a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx index 18f0a1d6b7646..94c38af18f2f7 100644 --- a/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx +++ b/apps/wallet/src/ui/app/staking/validators/ValidatorsCard.tsx @@ -4,9 +4,12 @@ import { useFeature } from '@growthbook/growthbook-react'; import { useMemo } from 'react'; +import { getStakingRewards } from '../getStakingRewards'; import { StakeAmount } from '../home/StakeAmount'; import { useGetDelegatedStake } from '../useGetDelegatedStake'; -import { DelegationCard, DelegationState } from './../home/DelegationCard'; +import { STATE_OBJECT } from '../usePendingDelegation'; +import { validatorsFields } from '../validatorsFields'; +import { DelegationCard } from './../home/DelegationCard'; import BottomMenuLayout, { Menu, Content, @@ -17,36 +20,58 @@ import { Text } from '_app/shared/text'; import Alert from '_components/alert'; import Icon, { SuiIcons } from '_components/icon'; import LoadingIndicator from '_components/loading/LoadingIndicator'; -import { useAppSelector } from '_hooks'; +import { useAppSelector, useGetObject } from '_hooks'; import { FEATURES } from '_src/shared/experimentation/features'; export function ValidatorsCard() { const accountAddress = useAppSelector(({ account }) => account.address); const { - data: stakeValidators, + data: delegations, isLoading, isError, error, } = useGetDelegatedStake(accountAddress || ''); + const { data: validators } = useGetObject(STATE_OBJECT); + + const validatorsData = validators && validatorsFields(validators); + + const activeValidators = + validatorsData?.validators.fields.active_validators; + // Total earn token for all delegations + const totalEarnToken = useMemo(() => { + if (!delegations || !validatorsData) return 0; + + const activeValidators = + validatorsData.validators.fields.active_validators; + + return delegations.reduce( + (acc, delegation) => + acc + getStakingRewards(activeValidators, delegation), + 0 + ); + }, [delegations, validatorsData]); + + // Total active stake for all delegations + const totalActivePendingStake = useMemo(() => { - if (!stakeValidators) return 0n; - return stakeValidators.reduce( + if (!delegations) return 0n; + return delegations.reduce( (acc, { staked_sui }) => acc + BigInt(staked_sui.principal.value), 0n ); - }, [stakeValidators]); + }, [delegations]); const numberOfValidators = useMemo(() => { - if (!stakeValidators) return 0; + if (!delegations) return 0; return [ ...new Set( - stakeValidators.map( + delegations.map( ({ staked_sui }) => staked_sui.validator_address ) ), ].length; - }, [stakeValidators]); + }, [delegations]); const stakingEnabled = useFeature(FEATURES.STAKING_ENABLED).on; @@ -69,7 +94,7 @@ export function ValidatorsCard() { } return ( -
+
@@ -94,13 +119,13 @@ export function ValidatorsCard() { @@ -108,22 +133,16 @@ export function ValidatorsCard() {
- {stakeValidators.map( - ({ delegation_status, staked_sui }) => ( + {validatorsData && + activeValidators && + delegations.map((delegationObject) => ( - ) - )} + ))}
diff --git a/apps/wallet/src/ui/app/staking/validatorsFields.ts b/apps/wallet/src/ui/app/staking/validatorsFields.ts new file mode 100644 index 0000000000000..d986ff21cf9f2 --- /dev/null +++ b/apps/wallet/src/ui/app/staking/validatorsFields.ts @@ -0,0 +1,19 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { + is, + SuiObject, + type ValidatorsFields, + type GetObjectDataResponse, +} from '@mysten/sui.js'; + +export function validatorsFields( + data?: GetObjectDataResponse +): ValidatorsFields | null { + return data && + is(data.details, SuiObject) && + data.details.data.dataType === 'moveObject' + ? (data.details.data.fields as ValidatorsFields) + : null; +}