Skip to content

Commit

Permalink
[wallet-ext] - add % stake share to validator list (MystenLabs#8091)
Browse files Browse the repository at this point in the history
<img width="366" alt="image"
src="https://user-images.githubusercontent.com/122397493/216756485-460432c4-8145-4798-9311-55da13501902.png">

displays % stake share on validator list view.
  • Loading branch information
mamos-mysten authored Feb 9, 2023
1 parent a8d81e6 commit 37fa21e
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 98 deletions.
10 changes: 0 additions & 10 deletions apps/wallet/src/shared/formatting.ts

This file was deleted.

18 changes: 18 additions & 0 deletions apps/wallet/src/ui/app/staking/calculateStakeShare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import BigNumber from 'bignumber.js';

export const calculateStakeShare = (
validatorStake: bigint,
totalStake: bigint,
decimalPlaces = 3
) => {
const bn = new BigNumber(validatorStake.toString());
const bd = new BigNumber(totalStake.toString());
const percentage = bn
.div(bd)
.multipliedBy(100)
.decimalPlaces(decimalPlaces)
.toNumber();
return percentage;
};
147 changes: 80 additions & 67 deletions apps/wallet/src/ui/app/staking/validators/SelectValidatorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import cl from 'classnames';
import { useState, useMemo } from 'react';

import { calculateAPY } from '../calculateAPY';
import { calculateStakeShare } from '../calculateStakeShare';
import { STATE_OBJECT, getName } from '../usePendingDelegation';
import { ValidatorListItem } from './ValidatorListItem';
import { Content, Menu } from '_app/shared/bottom-menu-layout';
Expand All @@ -16,13 +17,19 @@ import Icon, { SuiIcons } from '_components/icon';
import LoadingIndicator from '_components/loading/LoadingIndicator';
import { useGetObject } from '_hooks';

type SortKeys = 'name' | 'stakeShare' | 'apy';
const sortKeys: Record<SortKeys, string> = {
name: 'Name',
stakeShare: 'Stake Share',
apy: 'APY',
};

export function SelectValidatorCard() {
const [selectedValidator, setSelectedValidator] = useState<null | string>(
null
);
const [sortKey, setSortKey] = useState<'name' | 'apy'>('apy');
const [sortKey, setSortKey] = useState<SortKeys>('stakeShare');
const [sortAscending, setSortAscending] = useState(true);

const { data, isLoading, isError } = useGetObject(STATE_OBJECT);

const validatorsData = data && validatorsFields(data);
Expand All @@ -31,13 +38,25 @@ export function SelectValidatorCard() {
setSelectedValidator((state) => (state !== address ? address : null));
};

const handleSortByKey = (key: 'name' | 'apy') => {
const handleSortByKey = (key: SortKeys) => {
if (key === sortKey) {
setSortAscending(!sortAscending);
}
setSortKey(key);
};

const totalStake = useMemo(() => {
if (!validatorsData) return 0;
return validatorsData?.validators.fields.active_validators.reduce(
(acc, curr) =>
(acc +=
BigInt(
curr.fields.delegation_staking_pool.fields.sui_balance
) + BigInt(curr.fields.stake_amount)),
0n
);
}, [validatorsData]);

const validatorList = useMemo(() => {
if (!validatorsData) return [];

Expand All @@ -46,6 +65,13 @@ export function SelectValidatorCard() {
name: getName(validator.fields.metadata.fields.name),
address: validator.fields.metadata.fields.sui_address,
apy: calculateAPY(validator, +validatorsData.epoch),
stakeShare: calculateStakeShare(
BigInt(
validator.fields.delegation_staking_pool.fields
.sui_balance
) + BigInt(validator.fields.stake_amount),
BigInt(totalStake)
),
logo:
validator.fields.metadata.fields.image_url &&
typeof validator.fields.metadata.fields.image_url ===
Expand All @@ -63,7 +89,7 @@ export function SelectValidatorCard() {
return a[sortKey] - b[sortKey];
});
return sortAscending ? sortedAsc : sortedAsc.reverse();
}, [sortAscending, sortKey, validatorsData]);
}, [sortAscending, sortKey, validatorsData, totalStake]);

if (isLoading) {
return (
Expand Down Expand Up @@ -98,64 +124,42 @@ export function SelectValidatorCard() {
Sort by:
</Text>
<div className="flex items-center ml-2 gap-1.5">
<button
className="bg-transparent border-0 p-0 flex gap-1 cursor-pointer"
onClick={() => handleSortByKey('apy')}
>
<Text
variant="caption"
weight="medium"
color={
sortKey === 'apy'
? 'hero'
: 'steel-darker'
}
>
APY
</Text>
{sortKey === 'apy' && (
<Icon
icon={SuiIcons.ArrowLeft}
className={cl(
'text-captionSmall font-thin text-hero',
sortAscending
? 'rotate-90'
: '-rotate-90'
{Object.entries(sortKeys).map(([key, value]) => {
return (
<button
key={key}
className="bg-transparent border-0 p-0 flex gap-1 cursor-pointer"
onClick={() =>
handleSortByKey(key as SortKeys)
}
>
<Text
variant="caption"
weight="medium"
color={
sortKey === key
? 'hero'
: 'steel-darker'
}
>
{value}
</Text>
{sortKey === key && (
<Icon
icon={SuiIcons.ArrowLeft}
className={cl(
'text-captionSmall font-thin text-hero',
sortAscending
? 'rotate-90'
: '-rotate-90'
)}
/>
)}
/>
)}
</button>

<button
className="bg-transparent border-0 p-0 flex gap-1 cursor-pointer"
onClick={() => handleSortByKey('name')}
>
<Text
variant="caption"
weight="medium"
color={
sortKey === 'name'
? 'hero'
: 'steel-darker'
}
>
Name
</Text>
{sortKey === 'name' && (
<Icon
icon={SuiIcons.ArrowLeft}
className={cl(
'text-captionSmall font-thin text-hero',
sortAscending
? 'rotate-90'
: '-rotate-90'
)}
/>
)}
</button>
</button>
);
})}
</div>
</div>

<div className="flex items-start w-full">
<Text
variant="subtitle"
Expand All @@ -168,18 +172,27 @@ export function SelectValidatorCard() {
</div>
<div className="flex items-start flex-col w-full mt-1 flex-1">
{validatorsData &&
validatorList.map(({ name, address, apy, logo }) => (
validatorList.map((validator) => (
<div
className="cursor-pointer w-full relative"
key={address}
onClick={() => selectValidator(address)}
key={validator.address}
onClick={() =>
selectValidator(validator.address)
}
>
<ValidatorListItem
selected={selectedValidator === address}
validatorAddress={address}
validatorName={name}
logo={logo}
apy={apy}
selected={
selectedValidator === validator.address
}
validatorAddress={validator.address}
validatorName={validator.name}
label={sortKey}
value={
sortKey === 'name'
? '-'
: `${validator[sortKey]}%`
}
logo={validator.logo}
/>
</div>
))}
Expand Down
28 changes: 7 additions & 21 deletions apps/wallet/src/ui/app/staking/validators/ValidatorListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { motion, AnimatePresence } from 'framer-motion';

import { ImageIcon } from '_app/shared/image-icon';
import { Text } from '_app/shared/text';
import { IconTooltip } from '_app/shared/tooltip';
import ExplorerLink from '_components/explorer-link';
import { ExplorerLinkType } from '_components/explorer-link/ExplorerLinkType';
import Icon, { SuiIcons } from '_components/icon';
Expand All @@ -17,16 +16,17 @@ const TRUNCATE_PREFIX_LENGTH = 6;

type ValidatorListItemProp = {
selected?: boolean;
// APY can be N/A
apy: number;
label: string;
value: string | number;
validatorName: string;
validatorAddress: string;
logo: string | null;
};
export function ValidatorListItem({
selected,
validatorName,
apy,
label,
value,
logo,
validatorAddress,
}: ValidatorListItemProp) {
Expand Down Expand Up @@ -87,34 +87,20 @@ export function ValidatorListItem({
</div>
</div>
<div className="flex gap-0.5 items-center">
{typeof apy !== 'string' && (
<div className="flex gap-0.5 leading-none">
<Text
variant="body"
weight="semibold"
color="steel-darker"
>
{apy}
</Text>
)}
<div className="flex gap-0.5 leading-none">
<Text
variant="subtitleSmall"
weight="medium"
color="steel-dark"
>
{typeof apy === 'string' ? apy : '% APY'}
{value}
</Text>
<div
className={cl(
selected && '!opacity-100',
'text-steel items-baseline text-subtitle h-3 flex opacity-0 group-hover:opacity-100'
)}
>
<IconTooltip
tip="Annual Percentage Yield"
placement="top"
/>
</div>
></div>
</div>
</div>
</div>
Expand Down

0 comments on commit 37fa21e

Please sign in to comment.