Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(explorer): add validators joining next epoch #5269

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export * from './useGetAllStardustSharedObjects';
export * from './useGetStardustSharedBasicObjects';
export * from './useGetStardustSharedNftObjects';
export * from './useMaxTransactionSizeBytes';
export * from './useMultiGetNormalizedObjects';

export * from './stake';
export * from './ui';
45 changes: 45 additions & 0 deletions apps/core/src/hooks/useMultiGetNormalizedObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Mysten Labs, Inc.
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useIotaClient } from '@iota/dapp-kit';
import { IotaObjectDataOptions, IotaObjectResponse } from '@iota/iota-sdk/client';
import { normalizeIotaAddress } from '@iota/iota-sdk/utils';
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { chunkArray } from '../utils/chunkArray';

const defaultOptions = {
showType: true,
showContent: true,
showOwner: true,
showPreviousTransaction: true,
showStorageRebate: true,
showDisplay: true,
};

export function useMultiGetNormalizedObjects(
ids: string[],
options: IotaObjectDataOptions = defaultOptions,
queryOptions?: Omit<UseQueryOptions<IotaObjectResponse[]>, 'queryKey' | 'queryFn'>,
) {
const client = useIotaClient();

const normalizedIds = ids.map((id) => normalizeIotaAddress(id));

return useQuery({
...queryOptions,
queryKey: ['multiGetObjects', normalizedIds],
queryFn: async () => {
const responses = await Promise.all(
chunkArray(normalizedIds, 50).map((chunk) =>
client.multiGetObjects({
ids: chunk,
options,
}),
),
);
return responses.flat();
},
enabled: normalizedIds.length > 0,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
import { Badge, BadgeType, TableCellBase, TableCellText } from '@iota/apps-ui-kit';
import type { ColumnDef } from '@tanstack/react-table';
import { type ApyByValidator, formatPercentageDisplay, ImageIcon, ImageIconSize } from '@iota/core';
import { ampli, getValidatorMoveEvent, VALIDATOR_LOW_STAKE_GRACE_PERIOD } from '~/lib';
import {
ampli,
getValidatorMoveEvent,
type IotaValidatorSummaryExtended,
VALIDATOR_LOW_STAKE_GRACE_PERIOD,
} from '~/lib';
import { StakeColumn } from '~/components';
import type { IotaEvent, IotaValidatorSummary } from '@iota/iota-sdk/dist/cjs/client';
import type { IotaEvent } from '@iota/iota-sdk/client';
import clsx from 'clsx';
import { ValidatorLink } from '~/components/ui';

Expand All @@ -24,10 +29,29 @@
validator,
highlightValidatorName,
}: {
validator: IotaValidatorSummary;
validator: IotaValidatorSummaryExtended;
highlightValidatorName?: boolean;
}) {
return (
return validator.isPending ? (
<div className="dark:text-neutral-60 flex items-center gap-x-2.5 text-neutral-40">

Check failure on line 36 in apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx

View workflow job for this annotation

GitHub Actions / Lint, Build, and Test

Replace `dark:text-neutral-60·flex·items-center·gap-x-2.5·text-neutral-4` with `flex·items-center·gap-x-2.5·text-neutral-40·dark:text-neutral-6`
<div className="h-8 w-8 shrink-0">
<ImageIcon
src={validator.imageUrl}
label={validator.name}
fallback={validator.name}
size={ImageIconSize.Medium}
rounded
/>
</div>
<span
className={clsx('text-label-lg', {
'dark:text-neutral-92 text-neutral-10': highlightValidatorName,

Check failure on line 48 in apps/explorer/src/lib/ui/utils/generateValidatorsTableColumns.tsx

View workflow job for this annotation

GitHub Actions / Lint, Build, and Test

Replace `dark:text-neutral-92·text-neutral-10` with `text-neutral-10·dark:text-neutral-92`
})}
>
{validator.name}
</span>
</div>
) : (
<ValidatorLink
address={validator.iotaAddress}
onClick={() =>
Expand Down Expand Up @@ -68,8 +92,8 @@
showValidatorIcon = true,
includeColumns,
highlightValidatorName,
}: generateValidatorsTableColumnsArgs): ColumnDef<IotaValidatorSummary>[] {
let columns: ColumnDef<IotaValidatorSummary>[] = [
}: generateValidatorsTableColumnsArgs): ColumnDef<IotaValidatorSummaryExtended>[] {
let columns: ColumnDef<IotaValidatorSummaryExtended>[] = [
{
header: '#',
id: 'number',
Expand Down Expand Up @@ -209,6 +233,15 @@
const atRisk = isAtRisk
? VALIDATOR_LOW_STAKE_GRACE_PERIOD - Number(atRiskValidator[1])
: null;
const isPending = validator.isPending;

if (isPending) {
return (
<TableCellBase>
<Badge type={BadgeType.PrimarySoft} label="Pending" />
</TableCellBase>
);
}

if (atRisk === null) {
return (
Expand Down
1 change: 1 addition & 0 deletions apps/explorer/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from './sentry';
export * from './stringUtils';
export * from './iotaMoveTypeConverters';
export * from './getSupplyChangeAfterEpochEnd';
export * from './sanitizePendingValidators';
65 changes: 65 additions & 0 deletions apps/explorer/src/lib/utils/sanitizePendingValidators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import type { IotaObjectResponse, IotaValidatorSummary } from '@iota/iota-sdk/src/client';

export type IotaValidatorSummaryExtended = IotaValidatorSummary & { isPending?: boolean };

export function sanitizePendingValidators(
allPendings: IotaObjectResponse[] | undefined,
): IotaValidatorSummaryExtended[] {
return (
allPendings?.map(({ data }) => {
// const fieldsData =
// data?.content?.dataType === 'moveObject'
// ? (data?.content?.fields as Record<string, string | number | object>)
// : null;

const fields = data?.content?.fields?.value?.fields || {};
const metadata = fields.metadata?.fields || {};
const stakingPool = fields.staking_pool?.fields || {};
const exchangeRates = stakingPool.exchange_rates?.fields || {};

return {
isPending: true,
authorityPubkeyBytes: '',
commissionRate: fields.commission_rate,
description: metadata.description,
exchangeRatesId: exchangeRates.id?.id,
exchangeRatesSize: exchangeRates.size,
gasPrice: fields.gas_price,
imageUrl: metadata.image_url,
iotaAddress: metadata.iota_address,
name: metadata.name,
netAddress: metadata.net_address,
networkPubkeyBytes: '',
nextEpochAuthorityPubkeyBytes: metadata.next_epoch_authority_pubkey_bytes || null,
nextEpochCommissionRate: fields.next_epoch_commission_rate,
nextEpochGasPrice: fields.next_epoch_gas_price,
nextEpochNetAddress: metadata.next_epoch_net_address || null,
nextEpochNetworkPubkeyBytes: metadata.next_epoch_network_pubkey_bytes || null,
nextEpochP2pAddress: metadata.next_epoch_p2p_address || null,
nextEpochPrimaryAddress: metadata.next_epoch_primary_address || null,
nextEpochProofOfPossession: metadata.next_epoch_proof_of_possession || null,
nextEpochProtocolPubkeyBytes: metadata.next_epoch_protocol_pubkey_bytes || null,
nextEpochStake: fields.next_epoch_stake,
operationCapId: fields.operation_cap_id,
p2pAddress: metadata.p2p_address,
pendingPoolTokenWithdraw: stakingPool.pending_pool_token_withdraw,
pendingStake: stakingPool.pending_stake,
pendingTotalIotaWithdraw: stakingPool.pending_total_iota_withdraw,
poolTokenBalance: stakingPool.pool_token_balance,
primaryAddress: metadata.primary_address,
projectUrl: metadata.project_url,
proofOfPossessionBytes: '',
protocolPubkeyBytes: '',
rewardsPool: stakingPool.rewards_pool,
stakingPoolActivationEpoch: stakingPool.activation_epoch || null,
stakingPoolDeactivationEpoch: stakingPool.deactivation_epoch || null,
stakingPoolId: stakingPool.id?.id,
stakingPoolIotaBalance: stakingPool.iota_balance,
votingPower: fields.voting_power,
};
}) || []
);
}
33 changes: 30 additions & 3 deletions apps/explorer/src/pages/validators/Validators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
// SPDX-License-Identifier: Apache-2.0

import { type JSX, useMemo } from 'react';
import { roundFloat, useFormatCoin, useGetValidatorsApy, useGetValidatorsEvents } from '@iota/core';
import {
roundFloat,
useFormatCoin,
useGetDynamicFields,
useGetValidatorsApy,
useGetValidatorsEvents,
useMultiGetNormalizedObjects,
} from '@iota/core';
import {
DisplayStats,
DisplayStatsSize,
Expand All @@ -22,10 +29,12 @@
import { Warning } from '@iota/apps-ui-icons';
import { useQuery } from '@tanstack/react-query';
import { useEnhancedRpcClient } from '~/hooks';
import { sanitizePendingValidators } from '~/lib';

function ValidatorPageResult(): JSX.Element {
const { data, isPending, isSuccess, isError } = useIotaClientQuery('getLatestIotaSystemState');
const numberOfValidators = data?.activeValidators.length || 0;
let activeValidatorsData = data?.activeValidators;

const {
data: validatorEvents,
Expand All @@ -36,6 +45,20 @@
order: 'descending',
});

const { data: pendingActiveValidatorsId } = useGetDynamicFields(
data?.pendingActiveValidatorsId || '',
);
const pendingValidatorsObjectIdsData = pendingActiveValidatorsId?.pages[0]?.data || [];
const pendingValidatorsObjectIds = pendingValidatorsObjectIdsData.map((item) => item.objectId);
const { data: pendingValidatorsData } = useMultiGetNormalizedObjects(
pendingValidatorsObjectIds,
);

const sanitizePendingValidatorsData = sanitizePendingValidators(pendingValidatorsData);

console.log('pendingValidatorsData', pendingActiveValidatorsId)

Check failure on line 59 in apps/explorer/src/pages/validators/Validators.tsx

View workflow job for this annotation

GitHub Actions / Lint, Build, and Test

Replace `console.log('pendingValidatorsData',·pendingActiveValidatorsId)` with `····console.log('pendingValidatorsData',·pendingActiveValidatorsId);`
console.log('pendingValidators', pendingValidatorsObjectIdsData)

Check failure on line 60 in apps/explorer/src/pages/validators/Validators.tsx

View workflow job for this annotation

GitHub Actions / Lint, Build, and Test

Replace `console.log('pendingValidators',·pendingValidatorsObjectIdsData)` with `····console.log('pendingValidators',·pendingValidatorsObjectIdsData);`

const { data: validatorsApy } = useGetValidatorsApy();

const totalStaked = useMemo(() => {
Expand Down Expand Up @@ -82,7 +105,11 @@
const lastEpochRewardOnAllValidators =
epochData?.data[0].endOfEpochInfo?.totalStakeRewardsDistributed;

const tableData = data ? [...data.activeValidators].sort(() => 0.5 - Math.random()) : [];
if (data && Number(data.pendingActiveValidatorsSize) > 0) {
activeValidatorsData = [...data.activeValidators, ...sanitizePendingValidatorsData];
}

const tableData = data ? activeValidatorsData?.sort(() => 0.5 - Math.random()) : [];

const tableColumns = useMemo(() => {
if (!data || !validatorEvents) return null;
Expand Down Expand Up @@ -152,7 +179,7 @@
/>
) : (
<div className="flex w-full flex-col gap-xl">
<div className="py-md--rs text-display-sm text-neutral-10 dark:text-neutral-92">
<div className="dark:text-neutral-92 py-md--rs text-display-sm text-neutral-10">

Check failure on line 182 in apps/explorer/src/pages/validators/Validators.tsx

View workflow job for this annotation

GitHub Actions / Lint, Build, and Test

Replace `dark:text-neutral-92·py-md--rs·text-display-sm·text-neutral-10` with `py-md--rs·text-display-sm·text-neutral-10·dark:text-neutral-92`
Validators
</div>
<div className="flex w-full flex-col gap-md--rs md:h-40 md:flex-row">
Expand Down
Loading