From 9066a3e5790802651a4dedae05a9699891c67078 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Apr 2024 09:29:22 +0700 Subject: [PATCH 01/14] chore: add ExternalLinkIcon component in webb-ui-components --- .../nomination/[validatorAddress]/page.tsx | 2 ++ .../services/[serviceId]/JobsListTable.tsx | 12 ++------- .../ValidatorTable/ValidatorTable.tsx | 1 + .../WalletDropdown/WalletDropdown.tsx | 11 +++----- .../DelegateTxContainer/AuthorizeTx.tsx | 6 ++--- .../ExternalLinkIcon/ExternalLinkIcon.tsx | 23 ++++++++++++++++ .../src/components/ExternalLinkIcon/index.ts | 3 +++ .../src/components/index.ts | 1 + .../molecules/ExternalLinkIcon.stories.tsx | 26 +++++++++++++++++++ 9 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 libs/webb-ui-components/src/components/ExternalLinkIcon/ExternalLinkIcon.tsx create mode 100644 libs/webb-ui-components/src/components/ExternalLinkIcon/index.ts create mode 100644 libs/webb-ui-components/src/stories/molecules/ExternalLinkIcon.stories.tsx diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx index 4ef75ad2ad..6d913aedfc 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx @@ -6,6 +6,8 @@ import RoleDistributionCard from './RoleDistributionCard'; import ServiceTableTabs from './ServiceTableTabs'; import ValidatorOverviewCard from './ValidatorOverviewCard'; +// TODO: might need to add metadata here + export default function ValidatorDetails({ params, }: { diff --git a/apps/tangle-dapp/app/services/[serviceId]/JobsListTable.tsx b/apps/tangle-dapp/app/services/[serviceId]/JobsListTable.tsx index 3e13249f71..40528f30db 100644 --- a/apps/tangle-dapp/app/services/[serviceId]/JobsListTable.tsx +++ b/apps/tangle-dapp/app/services/[serviceId]/JobsListTable.tsx @@ -9,8 +9,8 @@ import { import getExplorerURI from '@webb-tools/api-provider-environment/transaction/utils/getExplorerURI'; import { chainsConfig } from '@webb-tools/dapp-config/chains'; import { PresetTypedChainId } from '@webb-tools/dapp-types'; -import { ExternalLinkLine } from '@webb-tools/icons'; import { + ExternalLinkIcon, fuzzyFilter, shortenHex, Table, @@ -72,15 +72,7 @@ const JobsListTable: FC = ({ serviceId, className }) => { return (
- {txExplorerURI && ( - - - - )} + {txExplorerURI && }
); }, diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx index 5c79c3a5aa..c1576393bf 100644 --- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx +++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx @@ -88,6 +88,7 @@ const ValidatorTable: FC = ({ data }) => { const onRowClick = useCallback( (row: Row) => { + // TODO: remove this check when the page is ready if (process.env.NODE_ENV === 'production') { return; } diff --git a/apps/tangle-dapp/components/WalletDropdown/WalletDropdown.tsx b/apps/tangle-dapp/components/WalletDropdown/WalletDropdown.tsx index 9fef708acf..9310a5d8b2 100644 --- a/apps/tangle-dapp/components/WalletDropdown/WalletDropdown.tsx +++ b/apps/tangle-dapp/components/WalletDropdown/WalletDropdown.tsx @@ -4,17 +4,14 @@ import { Trigger as DropdownTrigger } from '@radix-ui/react-dropdown-menu'; import { useWebContext } from '@webb-tools/api-provider-environment'; import { ManagedWallet, WalletConfig } from '@webb-tools/dapp-config'; import { WebbError, WebbErrorCodes } from '@webb-tools/dapp-types'; -import { - ExternalLinkLine, - LoginBoxLineIcon, - WalletLineIcon, -} from '@webb-tools/icons'; +import { LoginBoxLineIcon, WalletLineIcon } from '@webb-tools/icons'; import { useWallets } from '@webb-tools/react-hooks'; import { isViemError, WebbWeb3Provider } from '@webb-tools/web3-api-provider'; import { Button, Dropdown, DropdownBody, + ExternalLinkIcon, KeyValueWithButton, MenuItem, shortenString, @@ -97,9 +94,7 @@ export const WalletDropdown: FC<{ shortenFn={(str) => shortenString(str, 5)} /> - - - + diff --git a/apps/tangle-dapp/containers/DelegateTxContainer/AuthorizeTx.tsx b/apps/tangle-dapp/containers/DelegateTxContainer/AuthorizeTx.tsx index b5e0b63c50..6bdb83a138 100644 --- a/apps/tangle-dapp/containers/DelegateTxContainer/AuthorizeTx.tsx +++ b/apps/tangle-dapp/containers/DelegateTxContainer/AuthorizeTx.tsx @@ -1,7 +1,7 @@ import { isEthereumAddress } from '@polkadot/util-crypto'; -import { ExternalLinkLine } from '@webb-tools/icons/ExternalLinkLine'; import { CopyWithTooltip, + ExternalLinkIcon, InputField, Typography, } from '@webb-tools/webb-ui-components'; @@ -33,9 +33,7 @@ const AuthorizeTx: FC = ({ } - - - + )} diff --git a/libs/webb-ui-components/src/components/ExternalLinkIcon/ExternalLinkIcon.tsx b/libs/webb-ui-components/src/components/ExternalLinkIcon/ExternalLinkIcon.tsx new file mode 100644 index 0000000000..74c14fbef3 --- /dev/null +++ b/libs/webb-ui-components/src/components/ExternalLinkIcon/ExternalLinkIcon.tsx @@ -0,0 +1,23 @@ +import { ExternalLinkLine } from '@webb-tools/icons'; +import { IconSize } from '@webb-tools/icons/types'; +import { FC } from 'react'; + +interface ExternalLinkIconProps { + href: string; + size?: IconSize; + className?: string; +} + +const ExternalLinkIcon: FC = ({ + href, + size = 'md', + className, +}) => { + return ( + + + + ); +}; + +export default ExternalLinkIcon; diff --git a/libs/webb-ui-components/src/components/ExternalLinkIcon/index.ts b/libs/webb-ui-components/src/components/ExternalLinkIcon/index.ts new file mode 100644 index 0000000000..cc0b4f6bcc --- /dev/null +++ b/libs/webb-ui-components/src/components/ExternalLinkIcon/index.ts @@ -0,0 +1,3 @@ +import { default as ExternalLinkIcon } from './ExternalLinkIcon'; + +export default ExternalLinkIcon; diff --git a/libs/webb-ui-components/src/components/index.ts b/libs/webb-ui-components/src/components/index.ts index 3256391ab0..b9ceebd1a3 100644 --- a/libs/webb-ui-components/src/components/index.ts +++ b/libs/webb-ui-components/src/components/index.ts @@ -29,6 +29,7 @@ export * from './Drawer'; export * from './Dropdown'; export * from './DropdownMenu'; export * from './ErrorFallback'; +export { default as ExternalLinkIcon } from './ExternalLinkIcon'; export { default as FeeDetails } from './FeeDetails'; export * from './FeeDetails'; export * from './FileUploads'; diff --git a/libs/webb-ui-components/src/stories/molecules/ExternalLinkIcon.stories.tsx b/libs/webb-ui-components/src/stories/molecules/ExternalLinkIcon.stories.tsx new file mode 100644 index 0000000000..0fac8f6a6a --- /dev/null +++ b/libs/webb-ui-components/src/stories/molecules/ExternalLinkIcon.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import ExternalLinkIcon from '../../components/ExternalLinkIcon'; +import { TANGLE_MKT_URL } from '../../constants'; + +const meta: Meta = { + title: 'Design System/Molecules/ExternalLinkIcon', + component: ExternalLinkIcon, +}; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; + +export const Large: Story = { + render: () => , +}; + +export const ExtraLarge: Story = { + render: () => , +}; From 355f0f1f66af00a81e4db430dc28ed13c46c2a24 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Apr 2024 11:42:08 +0700 Subject: [PATCH 02/14] chore: add details column in validator table --- .../ValidatorTable/ValidatorTable.tsx | 113 +++++++++++------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx index c1576393bf..8f7be539e3 100644 --- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx +++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx @@ -6,52 +6,30 @@ import { getFilteredRowModel, getPaginationRowModel, getSortedRowModel, - Row, useReactTable, } from '@tanstack/react-table'; +import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction/utils'; import { Avatar, + Button, CopyWithTooltip, + ExternalLinkIcon, fuzzyFilter, shortenString, Table, Typography, } from '@webb-tools/webb-ui-components'; -import { useRouter } from 'next/navigation'; -import { FC, useCallback } from 'react'; +import Link from 'next/link'; +import { FC, useMemo } from 'react'; +import useNetworkStore from '../../context/useNetworkStore'; import { PagePath, Validator } from '../../types'; import { HeaderCell, StringCell } from '../tableCells'; import { ValidatorTableProps } from './types'; const columnHelper = createColumnHelper(); -const columns = [ - columnHelper.accessor('address', { - header: () => , - cell: (props) => { - const address = props.getValue(); - const identity = props.row.original.identityName; - - return ( -
- - hello - - - - {identity === address ? shortenString(address, 6) : identity} - - - -
- ); - }, - }), +const staticColumns = [ columnHelper.accessor('selfStaked', { header: () => , cell: (props) => ( @@ -81,21 +59,68 @@ const columns = [ /> ), }), + columnHelper.accessor('address', { + id: 'details', + header: () => null, + cell: (props) => ( +
+ + + +
+ ), + }), ]; const ValidatorTable: FC = ({ data }) => { - const router = useRouter(); + const { network } = useNetworkStore(); - const onRowClick = useCallback( - (row: Row) => { - // TODO: remove this check when the page is ready - if (process.env.NODE_ENV === 'production') { - return; - } + const columns = useMemo( + () => [ + columnHelper.accessor('address', { + header: () => , + cell: (props) => { + const address = props.getValue(); + const identity = props.row.original.identityName; + const accountExplorerLink = getExplorerURI( + network.polkadotExplorerUrl, + address, + 'address', + 'polkadot' + ).toString(); - router.push(`${PagePath.NOMINATION}/${row.original.address}`); - }, - [router] + return ( +
+ + + + {identity === address + ? shortenString(address, 6) + : formatIdentity(identity)} + + + + + +
+ ); + }, + }), + ...staticColumns, + ], + [network.polkadotExplorerUrl] ); const table = useReactTable({ @@ -121,10 +146,18 @@ const ValidatorTable: FC = ({ data }) => { paginationClassName="bg-mono-0 dark:bg-mono-180 pl-6" tableProps={table} isPaginated - onRowClick={onRowClick} /> ); }; export default ValidatorTable; + +/* @internal */ +function formatIdentity(inputString: string): string { + if (inputString.length > 15) { + return `${inputString.slice(0, 12)}...`; + } else { + return inputString; + } +} From 5a22484cb77edc0461b36094a27b8ef68848bce3 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Apr 2024 13:32:17 +0700 Subject: [PATCH 03/14] feat: get socials and name for validator card --- ...iewCard.tsx => ValidatorBasicInfoCard.tsx} | 42 +++++-------- .../nomination/[validatorAddress]/page.tsx | 4 +- .../data/NominationsPayouts/useNominations.ts | 4 +- .../data/NominationsPayouts/usePayouts.ts | 13 ++-- .../ValidatorDetails/useValidatorBasicInfo.ts | 63 +++++++++++++++++++ .../useValidatorIdentityNames.ts | 31 +++------ .../data/getValidatorOverviewData.ts | 31 --------- apps/tangle-dapp/data/payouts/usePayouts2.ts | 4 +- apps/tangle-dapp/utils/polkadot/identity.ts | 29 +++++++++ apps/tangle-dapp/utils/polkadot/index.ts | 1 + apps/tangle-dapp/utils/polkadot/nominators.ts | 9 ++- 11 files changed, 135 insertions(+), 96 deletions(-) rename apps/tangle-dapp/app/nomination/[validatorAddress]/{ValidatorOverviewCard.tsx => ValidatorBasicInfoCard.tsx} (78%) create mode 100644 apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts delete mode 100644 apps/tangle-dapp/data/getValidatorOverviewData.ts create mode 100644 apps/tangle-dapp/utils/polkadot/identity.ts diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorOverviewCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx similarity index 78% rename from apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorOverviewCard.tsx rename to apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx index 01c3d2029e..6f740786f3 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorOverviewCard.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx @@ -1,4 +1,5 @@ -import { MapPinLine } from '@webb-tools/icons'; +'use client'; + import { Avatar, Chip, @@ -6,33 +7,32 @@ import { Typography, } from '@webb-tools/webb-ui-components'; import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString'; +import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; import { SocialChip, TangleBigLogo } from '../../../components'; -import getValidatorOverviewData from '../../../data/getValidatorOverviewData'; +import useValidatorBasicInfo from '../../../data/ValidatorDetails/useValidatorBasicInfo'; import TotalRestaked from './TotalRestaked'; -interface ValidatorOverviewCardProps { +interface ValidatorBasicInfoCardProps { validatorAddress: string; className?: string; } -export default async function ValidatorOverviewCard({ +const ValidatorBasicInfoCard: FC = ({ validatorAddress, className, -}: ValidatorOverviewCardProps) { +}: ValidatorBasicInfoCardProps) => { const { - identity, + name, isActive, totalRestaked, restakingMethod, nominations, twitter, - discord, email, web, - location, - } = await getValidatorOverviewData(validatorAddress); + } = useValidatorBasicInfo(validatorAddress); return (
- {identity && ( + {name && ( - {identity} + {name} )} @@ -104,28 +104,16 @@ export default async function ValidatorOverviewCard({
{twitter && } - {discord && } {email && } {web && }
-
- {location && ( - - - - {location} - - - )} -
+ {/* TODO: get location later */}
); -} +}; + +export default ValidatorBasicInfoCard; diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx index 6d913aedfc..2a55c31949 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx @@ -4,7 +4,7 @@ import { notFound } from 'next/navigation'; import NodeSpecificationsTable from './NodeSpecificationsTable'; import RoleDistributionCard from './RoleDistributionCard'; import ServiceTableTabs from './ServiceTableTabs'; -import ValidatorOverviewCard from './ValidatorOverviewCard'; +import ValidatorBasicInfoCard from './ValidatorBasicInfoCard'; // TODO: might need to add metadata here @@ -22,7 +22,7 @@ export default function ValidatorDetails({ return (
- diff --git a/apps/tangle-dapp/data/NominationsPayouts/useNominations.ts b/apps/tangle-dapp/data/NominationsPayouts/useNominations.ts index 066cb4d4a1..fedc212351 100644 --- a/apps/tangle-dapp/data/NominationsPayouts/useNominations.ts +++ b/apps/tangle-dapp/data/NominationsPayouts/useNominations.ts @@ -15,7 +15,7 @@ import { getPolkadotApiRx, getTotalNumberOfNominators, getValidatorCommission, - getValidatorIdentity, + getValidatorIdentityName, } from '../../utils/polkadot'; export default function useNominations( @@ -73,7 +73,7 @@ export default function useNominations( ) ); - const identity = await getValidatorIdentity( + const identity = await getValidatorIdentityName( rpcEndpoint, target.toString() ); diff --git a/apps/tangle-dapp/data/NominationsPayouts/usePayouts.ts b/apps/tangle-dapp/data/NominationsPayouts/usePayouts.ts index 53b2278b03..720960695f 100644 --- a/apps/tangle-dapp/data/NominationsPayouts/usePayouts.ts +++ b/apps/tangle-dapp/data/NominationsPayouts/usePayouts.ts @@ -14,7 +14,7 @@ import { getPolkadotApiPromise, getPolkadotApiRx, getValidatorCommission, - getValidatorIdentity, + getValidatorIdentityName, } from '../../utils/polkadot'; export default function usePayouts( @@ -223,15 +223,16 @@ export default function usePayouts( nativeTokenSymbol ); - const validatorIdentity = await getValidatorIdentity( - rpcEndpoint, - validator - ); + const validatorIdentity = + await getValidatorIdentityName( + rpcEndpoint, + validator + ); const validatorNominators = await Promise.all( eraStaker.others.map(async (nominator) => { const nominatorIdentity = - await getValidatorIdentity( + await getValidatorIdentityName( rpcEndpoint, nominator.who.toString() ); diff --git a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts new file mode 100644 index 0000000000..72973e023e --- /dev/null +++ b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts @@ -0,0 +1,63 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import useNetworkStore from '../../context/useNetworkStore'; +import { getPolkadotApiPromise } from '../../utils/polkadot/api'; +import { + extractDataFromIdentityInfo, + IdentityDataType, +} from '../../utils/polkadot/identity'; + +type ValidatorOverviewDataType = { + identity?: string; + isActive: boolean; + totalRestaked: number; + restakingMethod?: 'independent' | 'shared'; + nominations: number; + twitter?: string; + discord?: string; + email?: string; + web?: string; + location?: string; +}; + +export default function useValidatorBasicInfo(validatorAddress: string) { + const { rpcEndpoint } = useNetworkStore(); + + const [name, setName] = useState(null); + const [email, setEmail] = useState(null); + const [web, setWeb] = useState(null); + const [twitter, setTwitter] = useState(null); + + useEffect(() => { + const fetchData = async () => { + const api = await getPolkadotApiPromise(rpcEndpoint); + const identityData = await api.query.identity.identityOf( + validatorAddress + ); + + if (identityData.isSome) { + const identity = identityData.unwrapOrDefault(); + const info = identity[0].info; + setName(extractDataFromIdentityInfo(info, IdentityDataType.NAME)); + setEmail(extractDataFromIdentityInfo(info, IdentityDataType.EMAIL)); + setWeb(extractDataFromIdentityInfo(info, IdentityDataType.WEB)); + setTwitter(extractDataFromIdentityInfo(info, IdentityDataType.TWITTER)); + } + }; + + fetchData(); + }, [validatorAddress, rpcEndpoint]); + + return { + name, + isActive: true, + totalRestaked: 1000, + restakingMethod: 'independent', + nominations: 155, + twitter, + email, + web, + }; +} diff --git a/apps/tangle-dapp/data/ValidatorTables/useValidatorIdentityNames.ts b/apps/tangle-dapp/data/ValidatorTables/useValidatorIdentityNames.ts index 0ee1790d1f..c571f0c7ad 100644 --- a/apps/tangle-dapp/data/ValidatorTables/useValidatorIdentityNames.ts +++ b/apps/tangle-dapp/data/ValidatorTables/useValidatorIdentityNames.ts @@ -1,33 +1,16 @@ import { Bytes, Option, StorageKey } from '@polkadot/types'; import { AccountId32 } from '@polkadot/types/interfaces'; -import { - PalletIdentityLegacyIdentityInfo, - PalletIdentityRegistration, -} from '@polkadot/types/lookup'; +import { PalletIdentityRegistration } from '@polkadot/types/lookup'; import { ITuple } from '@polkadot/types/types'; import { useCallback } from 'react'; import { map } from 'rxjs'; import useEntryMap from '../../hooks/useEntryMap'; import usePolkadotApiRx from '../../hooks/usePolkadotApiRx'; - -export const extractNameFromInfo = ( - info: PalletIdentityLegacyIdentityInfo -): string | null => { - const displayNameJson = info.display.toString(); - - const displayNameObject: { raw?: `0x${string}` } = - JSON.parse(displayNameJson); - - // If the display name is in hex format, convert it to a string. - if (displayNameObject.raw !== undefined) { - const hexString = displayNameObject.raw; - - return Buffer.from(hexString.slice(2), 'hex').toString('utf8'); - } - - return null; -}; +import { + extractDataFromIdentityInfo, + IdentityDataType, +} from '../../utils/polkadot/identity'; const mapIdentitiesToNames = ( identities: [ @@ -40,7 +23,9 @@ const mapIdentitiesToNames = ( return [ address.args[0].toString(), - info !== null ? extractNameFromInfo(info) : null, + info !== null + ? extractDataFromIdentityInfo(info, IdentityDataType.NAME) + : null, ]; }); diff --git a/apps/tangle-dapp/data/getValidatorOverviewData.ts b/apps/tangle-dapp/data/getValidatorOverviewData.ts deleted file mode 100644 index e92d5be276..0000000000 --- a/apps/tangle-dapp/data/getValidatorOverviewData.ts +++ /dev/null @@ -1,31 +0,0 @@ -type ValidatorOverviewDataType = { - identity?: string; - isActive: boolean; - totalRestaked: number; - restakingMethod?: 'independent' | 'shared'; - nominations: number; - twitter?: string; - discord?: string; - email?: string; - web?: string; - location?: string; -}; - -export default async function getValidatorOverviewData( - validatorAddress: string -): Promise { - // TODO: handle validatorAddress - console.log('validatorAddress :', validatorAddress); - return { - identity: 'validator1', - isActive: true, - totalRestaked: 1000, - restakingMethod: 'independent', - nominations: 155, - twitter: 'https://twitter.com/tangle_network', - discord: 'https://discord.com/invite/krp36ZSR8J', - email: 'someone@example.com', - web: 'https://tangle.tools/', - location: 'USA', - }; -} diff --git a/apps/tangle-dapp/data/payouts/usePayouts2.ts b/apps/tangle-dapp/data/payouts/usePayouts2.ts index bb015450cd..e63ff66a98 100644 --- a/apps/tangle-dapp/data/payouts/usePayouts2.ts +++ b/apps/tangle-dapp/data/payouts/usePayouts2.ts @@ -11,7 +11,7 @@ import { formatTokenBalance, getPolkadotApiPromise, getValidatorCommission, - getValidatorIdentity, + getValidatorIdentityName, } from '../../utils/polkadot'; import useValidatorIdentityNames from '../ValidatorTables/useValidatorIdentityNames'; import useEraTotalRewards from './useEraTotalRewards'; @@ -182,7 +182,7 @@ const usePayouts2 = () => { const validatorNominators = await Promise.all( eraStakers.others.map(async (nominator) => { - const nominatorIdentity = await getValidatorIdentity( + const nominatorIdentity = await getValidatorIdentityName( rpcEndpoint, nominator.who.toString() ); diff --git a/apps/tangle-dapp/utils/polkadot/identity.ts b/apps/tangle-dapp/utils/polkadot/identity.ts new file mode 100644 index 0000000000..32e98143f4 --- /dev/null +++ b/apps/tangle-dapp/utils/polkadot/identity.ts @@ -0,0 +1,29 @@ +import { PalletIdentityLegacyIdentityInfo } from '@polkadot/types/lookup'; + +export enum IdentityDataType { + NAME = 'display', + WEB = 'web', + EMAIL = 'email', + TWITTER = 'twitter', +} + +export const extractDataFromIdentityInfo = ( + info: PalletIdentityLegacyIdentityInfo, + type: IdentityDataType +): string | null => { + const displayData = info[type]; + if (displayData.isNone) return null; + + const displayDataObject: { raw?: string } = JSON.parse( + displayData.toString() + ); + + // If the display name is in hex format, convert it to a string. + if (displayDataObject.raw !== undefined) { + const hexString = displayDataObject.raw; + + return Buffer.from(hexString.slice(2), 'hex').toString('utf8'); + } + + return null; +}; diff --git a/apps/tangle-dapp/utils/polkadot/index.ts b/apps/tangle-dapp/utils/polkadot/index.ts index fd3a670fef..90497fbe7f 100644 --- a/apps/tangle-dapp/utils/polkadot/index.ts +++ b/apps/tangle-dapp/utils/polkadot/index.ts @@ -1,5 +1,6 @@ export * from './api'; export * from './bond'; +export * from './identity'; export * from './nominators'; export * from './payout'; export * from './tokens'; diff --git a/apps/tangle-dapp/utils/polkadot/nominators.ts b/apps/tangle-dapp/utils/polkadot/nominators.ts index 693be876f4..71af8ebd66 100644 --- a/apps/tangle-dapp/utils/polkadot/nominators.ts +++ b/apps/tangle-dapp/utils/polkadot/nominators.ts @@ -1,7 +1,7 @@ import type { HexString } from '@polkadot/util/types'; -import { extractNameFromInfo } from '../../data/ValidatorTables/useValidatorIdentityNames'; import { getPolkadotApiPromise } from './api'; +import { extractDataFromIdentityInfo, IdentityDataType } from './identity'; import { getTxPromise } from './utils'; export const getTotalNumberOfNominators = async ( @@ -29,7 +29,7 @@ export const getTotalNumberOfNominators = async ( return totalNominators.length; }; -export const getValidatorIdentity = async ( +export const getValidatorIdentityName = async ( rpcEndpoint: string, validatorAddress: string ): Promise => { @@ -41,7 +41,10 @@ export const getValidatorIdentity = async ( if (identityOpt.isSome) { const identity = identityOpt.unwrap(); const info = identity[0].info; - const displayName = extractNameFromInfo(info); + const displayName = extractDataFromIdentityInfo( + info, + IdentityDataType.NAME + ); if (displayName !== null) { return displayName; From acb274695b2fd4eaee05bdd721f9124220d76bb6 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Apr 2024 16:42:10 +0700 Subject: [PATCH 04/14] feat: total restaked for validator --- .../[validatorAddress]/TotalRestaked.tsx | 18 ----- .../ValidatorBasicInfoCard.tsx | 14 ++-- apps/tangle-dapp/app/restake/page.tsx | 26 ++---- .../ValidatorDetails/useValidatorBasicInfo.ts | 79 ++++++++++++------- .../data/restaking/useRestakingProfile.ts | 22 +++++- .../data/restaking/useRestakingRoleLedger.ts | 6 +- 6 files changed, 87 insertions(+), 78 deletions(-) delete mode 100644 apps/tangle-dapp/app/nomination/[validatorAddress]/TotalRestaked.tsx diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/TotalRestaked.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/TotalRestaked.tsx deleted file mode 100644 index 61ee4527a1..0000000000 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/TotalRestaked.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { Typography } from '@webb-tools/webb-ui-components'; -import { FC } from 'react'; - -import useNetworkStore from '../../../context/useNetworkStore'; - -const TotalRestaked: FC<{ totalRestaked: number }> = ({ totalRestaked }) => { - const { nativeTokenSymbol } = useNetworkStore(); - - return ( - - {totalRestaked} {nativeTokenSymbol} - - ); -}; - -export default TotalRestaked; diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx index 6f740786f3..07c08a9035 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx @@ -11,8 +11,8 @@ import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; import { SocialChip, TangleBigLogo } from '../../../components'; +import useNetworkStore from '../../../context/useNetworkStore'; import useValidatorBasicInfo from '../../../data/ValidatorDetails/useValidatorBasicInfo'; -import TotalRestaked from './TotalRestaked'; interface ValidatorBasicInfoCardProps { validatorAddress: string; @@ -23,6 +23,8 @@ const ValidatorBasicInfoCard: FC = ({ validatorAddress, className, }: ValidatorBasicInfoCardProps) => { + const { nativeTokenSymbol } = useNetworkStore(); + const { name, isActive, @@ -86,8 +88,10 @@ const ValidatorBasicInfoCard: FC = ({ Total Restaked
- - {restakingMethod ?? 'N/A'} + + {totalRestaked ?? "--"} {nativeTokenSymbol} + + {restakingMethod?.value ?? 'N/A'}
@@ -95,13 +99,13 @@ const ValidatorBasicInfoCard: FC = ({ Nominations - {nominations} + {nominations ?? '--'}
{/* Socials & Location */} -
+
{twitter && } {email && } diff --git a/apps/tangle-dapp/app/restake/page.tsx b/apps/tangle-dapp/app/restake/page.tsx index 8d258a3e46..1a1ddd69d7 100644 --- a/apps/tangle-dapp/app/restake/page.tsx +++ b/apps/tangle-dapp/app/restake/page.tsx @@ -18,6 +18,7 @@ const RestakePage = () => { hasExistingProfile, profileTypeOpt: substrateProfileTypeOpt, ledgerOpt, + totalRestaked, } = useRestakingProfile(); const { maxRestakingAmount } = useRestakingLimits(); @@ -33,18 +34,7 @@ const RestakePage = () => { return Object.values(earningsRecord).reduce((prev, curr) => prev + curr, 0); }, [earningsRecord, isEarningsLoading]); - const { availableForRestake, totalRestaked } = useMemo(() => { - const totalRestaked = ledgerOpt?.isSome - ? // Dummy check to whether format the total restaked amount - // or not, as the local testnet is in wei but the live one is in unit - ledgerOpt.unwrap().total.toString().length > 10 - ? +formatUnits( - ledgerOpt.unwrap().total.toBigInt(), - TANGLE_TOKEN_DECIMALS - ) - : ledgerOpt.unwrap().total.toNumber() - : null; - + const availableForRestake = useMemo(() => { const fmtMaxRestakingAmount = maxRestakingAmount !== null ? +formatUnits( @@ -59,17 +49,11 @@ const RestakePage = () => { ? fmtMaxRestakingAmount - totalRestaked : 0; - return { - totalRestaked, - availableForRestake, - }; + return availableForRestake; } - return { - totalRestaked, - availableForRestake: fmtMaxRestakingAmount, - }; - }, [ledgerOpt, maxRestakingAmount]); + return fmtMaxRestakingAmount; + }, [maxRestakingAmount, totalRestaked]); return (
diff --git a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts index 72973e023e..b65eeeed09 100644 --- a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts +++ b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts @@ -8,54 +8,75 @@ import { extractDataFromIdentityInfo, IdentityDataType, } from '../../utils/polkadot/identity'; - -type ValidatorOverviewDataType = { - identity?: string; - isActive: boolean; - totalRestaked: number; - restakingMethod?: 'independent' | 'shared'; - nominations: number; - twitter?: string; - discord?: string; - email?: string; - web?: string; - location?: string; -}; +import useRestakingProfile from '../restaking/useRestakingProfile'; +import useCurrentEra from '../staking/useCurrentEra'; export default function useValidatorBasicInfo(validatorAddress: string) { const { rpcEndpoint } = useNetworkStore(); + const { data: currentEra } = useCurrentEra(); + const { profileTypeOpt, totalRestaked } = + useRestakingProfile(validatorAddress); const [name, setName] = useState(null); const [email, setEmail] = useState(null); const [web, setWeb] = useState(null); const [twitter, setTwitter] = useState(null); + const [nominations, setNominations] = useState(null); useEffect(() => { const fetchData = async () => { const api = await getPolkadotApiPromise(rpcEndpoint); - const identityData = await api.query.identity.identityOf( - validatorAddress - ); - - if (identityData.isSome) { - const identity = identityData.unwrapOrDefault(); - const info = identity[0].info; - setName(extractDataFromIdentityInfo(info, IdentityDataType.NAME)); - setEmail(extractDataFromIdentityInfo(info, IdentityDataType.EMAIL)); - setWeb(extractDataFromIdentityInfo(info, IdentityDataType.WEB)); - setTwitter(extractDataFromIdentityInfo(info, IdentityDataType.TWITTER)); - } + const fetchNameAndSocials = async () => { + const identityData = await api.query.identity.identityOf( + validatorAddress + ); + + if (identityData.isSome) { + const identity = identityData.unwrap(); + const info = identity[0]?.info; + if (info) { + setName(extractDataFromIdentityInfo(info, IdentityDataType.NAME)); + setEmail(extractDataFromIdentityInfo(info, IdentityDataType.EMAIL)); + setWeb(extractDataFromIdentityInfo(info, IdentityDataType.WEB)); + setTwitter( + extractDataFromIdentityInfo(info, IdentityDataType.TWITTER) + ); + } + } + }; + + const fetchNominations = async () => { + if ( + currentEra !== null && + api.query.staking?.erasStakersOverview !== undefined + ) { + const erasStakersOverviewData = + await api.query.staking.erasStakersOverview( + currentEra, + validatorAddress + ); + if (erasStakersOverviewData.isSome) { + const nominatorCount = + erasStakersOverviewData.unwrap().nominatorCount; + setNominations(nominatorCount.toNumber()); + } + } else { + setNominations(0); + } + }; + + await Promise.all([fetchNameAndSocials(), fetchNominations()]); }; fetchData(); - }, [validatorAddress, rpcEndpoint]); + }, [validatorAddress, rpcEndpoint, currentEra]); return { name, isActive: true, - totalRestaked: 1000, - restakingMethod: 'independent', - nominations: 155, + totalRestaked, + restakingMethod: profileTypeOpt, + nominations, twitter, email, web, diff --git a/apps/tangle-dapp/data/restaking/useRestakingProfile.ts b/apps/tangle-dapp/data/restaking/useRestakingProfile.ts index 9a202ba7a6..f083f805c4 100644 --- a/apps/tangle-dapp/data/restaking/useRestakingProfile.ts +++ b/apps/tangle-dapp/data/restaking/useRestakingProfile.ts @@ -1,11 +1,13 @@ import { useMemo } from 'react'; +import { formatUnits } from 'viem'; +import { TANGLE_TOKEN_DECIMALS } from '../../constants'; import { RestakingProfileType } from '../../types'; import Optional from '../../utils/Optional'; import useRestakingRoleLedger from './useRestakingRoleLedger'; -const useRestakingProfile = () => { - const { data: ledgerOpt, isLoading } = useRestakingRoleLedger(); +const useRestakingProfile = (address?: string) => { + const { data: ledgerOpt, isLoading } = useRestakingRoleLedger(address); const hasExistingProfile = isLoading ? null @@ -27,10 +29,26 @@ const useRestakingProfile = () => { ); }, [ledgerOpt]); + const totalRestaked = useMemo( + () => + ledgerOpt?.isSome + ? // Dummy check to whether format the total restaked amount + // or not, as the local testnet is in wei but the live one is in unit + ledgerOpt.unwrap().total.toString().length > 10 + ? +formatUnits( + ledgerOpt.unwrap().total.toBigInt(), + TANGLE_TOKEN_DECIMALS + ) + : ledgerOpt.unwrap().total.toNumber() + : null, + [ledgerOpt] + ); + return { hasExistingProfile, profileTypeOpt, ledgerOpt, + totalRestaked, }; }; diff --git a/apps/tangle-dapp/data/restaking/useRestakingRoleLedger.ts b/apps/tangle-dapp/data/restaking/useRestakingRoleLedger.ts index 1bdf0f82c7..3506b5397c 100644 --- a/apps/tangle-dapp/data/restaking/useRestakingRoleLedger.ts +++ b/apps/tangle-dapp/data/restaking/useRestakingRoleLedger.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import usePolkadotApiRx from '../../hooks/usePolkadotApiRx'; import useSubstrateAddress from '../../hooks/useSubstrateAddress'; -const useRestakingRoleLedger = () => { +const useRestakingRoleLedger = (address?: string) => { const activeSubstrateAccount = useSubstrateAddress(); return usePolkadotApiRx( @@ -13,9 +13,9 @@ const useRestakingRoleLedger = () => { return null; } - return api.query.roles.ledger(activeSubstrateAccount); + return api.query.roles.ledger(address ?? activeSubstrateAccount); }, - [activeSubstrateAccount] + [address, activeSubstrateAccount] ) ); }; From a7f415bf44c608521720de22cf2510ba1110bf3a Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Apr 2024 17:03:37 +0700 Subject: [PATCH 05/14] chore: add explorer link insude validator card --- .../ValidatorBasicInfoCard.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx index 07c08a9035..02e1bc33f6 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx @@ -1,9 +1,11 @@ 'use client'; +import { getExplorerURI } from '@webb-tools/api-provider-environment/transaction/utils'; import { Avatar, Chip, CopyWithTooltip, + ExternalLinkIcon, Typography, } from '@webb-tools/webb-ui-components'; import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString'; @@ -23,7 +25,7 @@ const ValidatorBasicInfoCard: FC = ({ validatorAddress, className, }: ValidatorBasicInfoCardProps) => { - const { nativeTokenSymbol } = useNetworkStore(); + const { network, nativeTokenSymbol } = useNetworkStore(); const { name, @@ -72,11 +74,22 @@ const ValidatorBasicInfoCard: FC = ({ variant="h5" className="!text-mono-100" >{`Address: ${shortenString(validatorAddress, 7)}`} + + +
@@ -89,7 +102,7 @@ const ValidatorBasicInfoCard: FC = ({
- {totalRestaked ?? "--"} {nativeTokenSymbol} + {totalRestaked ?? '--'} {nativeTokenSymbol} {restakingMethod?.value ?? 'N/A'}
From 3bb0725a2732de32cb020179332594d717e46852 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Tue, 16 Apr 2024 17:16:15 +0700 Subject: [PATCH 06/14] chore: fix twitter link --- .../ValidatorDetails/useValidatorBasicInfo.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts index b65eeeed09..06b1d0634e 100644 --- a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts +++ b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts @@ -38,9 +38,20 @@ export default function useValidatorBasicInfo(validatorAddress: string) { setName(extractDataFromIdentityInfo(info, IdentityDataType.NAME)); setEmail(extractDataFromIdentityInfo(info, IdentityDataType.EMAIL)); setWeb(extractDataFromIdentityInfo(info, IdentityDataType.WEB)); - setTwitter( - extractDataFromIdentityInfo(info, IdentityDataType.TWITTER) + const twitterName = extractDataFromIdentityInfo( + info, + IdentityDataType.TWITTER ); + if (twitterName === null) { + setTwitter(twitterName); + } else { + // Convert twitter user name to corresponding href + // Example: @tangle_network -> https://twitter.com/tangle_network + const twitterHref = `https://twitter.com/${twitterName.substring( + 1 + )}`; + setTwitter(twitterHref); + } } } }; From 9fc63c72411cc44bdd83797c9387824e6a2d59dd Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Wed, 17 Apr 2024 13:56:37 +0700 Subject: [PATCH 07/14] chore: minor refactor --- .../ValidatorDetails/useValidatorBasicInfo.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts index 06b1d0634e..5fa5671ca4 100644 --- a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts +++ b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts @@ -42,16 +42,11 @@ export default function useValidatorBasicInfo(validatorAddress: string) { info, IdentityDataType.TWITTER ); - if (twitterName === null) { - setTwitter(twitterName); - } else { - // Convert twitter user name to corresponding href - // Example: @tangle_network -> https://twitter.com/tangle_network - const twitterHref = `https://twitter.com/${twitterName.substring( - 1 - )}`; - setTwitter(twitterHref); - } + setTwitter( + twitterName === null + ? null + : `https://twitter.com/${twitterName.substring(1)}` + ); } } }; From 57a05846c005b79eaefe7e108ec798b8d8f42ca5 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Wed, 17 Apr 2024 16:27:54 +0700 Subject: [PATCH 08/14] chore: add loading screen --- .../NodeSpecificationsTable.tsx | 4 +- .../ValidatorBasicInfoCard.tsx | 17 +---- .../nomination/[validatorAddress]/loading.tsx | 68 +++++++++++++++++++ .../tangle-dapp/components/GlassCard/index.ts | 4 +- apps/tangle-dapp/components/index.ts | 2 + 5 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/NodeSpecificationsTable.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/NodeSpecificationsTable.tsx index 4e33d46aa4..4796ee4345 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/NodeSpecificationsTable.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/NodeSpecificationsTable.tsx @@ -112,8 +112,8 @@ const NodeSpecificationsTable: FC = ({ }); return ( -
- +
+ Node Specifications diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx index 02e1bc33f6..7d729d80a9 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx @@ -12,7 +12,7 @@ import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenStrin import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; -import { SocialChip, TangleBigLogo } from '../../../components'; +import { SocialChip, TangleCard } from '../../../components'; import useNetworkStore from '../../../context/useNetworkStore'; import useValidatorBasicInfo from '../../../data/ValidatorDetails/useValidatorBasicInfo'; @@ -39,16 +39,7 @@ const ValidatorBasicInfoCard: FC = ({ } = useValidatorBasicInfo(validatorAddress); return ( -
+
= ({ {/* TODO: get location later */}
- - -
+ ); }; diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx new file mode 100644 index 0000000000..f8ee07a4f0 --- /dev/null +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx @@ -0,0 +1,68 @@ +import { Spinner } from '@webb-tools/icons'; +import { SkeletonLoader, Typography } from '@webb-tools/webb-ui-components'; + +import { GlassCard, TangleCard } from '../../../components'; + +export default function Loading() { + return ( +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+ + Total Restaked + + +
+
+ + Nominations + + +
+
+
+ + + + Role Distribution + +
+ +
+
+
+ +
+ + Node Specifications + + +
+ +
+
+ + Active Services + + + Past Services + +
+ +
+
+ ); +} diff --git a/apps/tangle-dapp/components/GlassCard/index.ts b/apps/tangle-dapp/components/GlassCard/index.ts index caf4d79a10..6a14fedf44 100644 --- a/apps/tangle-dapp/components/GlassCard/index.ts +++ b/apps/tangle-dapp/components/GlassCard/index.ts @@ -1 +1,3 @@ -export * from './GlassCard'; +import { default as GlassCard } from './GlassCard'; + +export default GlassCard; diff --git a/apps/tangle-dapp/components/index.ts b/apps/tangle-dapp/components/index.ts index d7e19077bd..18dc3ed271 100644 --- a/apps/tangle-dapp/components/index.ts +++ b/apps/tangle-dapp/components/index.ts @@ -1,6 +1,7 @@ export * from './BondedTokensBalanceInfo'; export * from './Breadcrumbs'; export * from './DelegatorTable'; +export { default as GlassCard } from './GlassCard'; export * from './HeaderChip'; export * from './InfoIconWithTooltip'; export * from './KeyStatsItem'; @@ -13,6 +14,7 @@ export * from './skeleton'; export { default as SocialChip } from './SocialChip'; export * from './TableStatus'; export { default as TangleBigLogo } from './TangleBigLogo'; +export { default as TangleCard } from './TangleCard'; export * from './UnbondingStatsItem'; export * from './ValidatorList'; export * from './ValidatorTable'; From e443d0c7e963a6412d016de21548fb6340d80b1d Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Wed, 17 Apr 2024 17:49:56 +0700 Subject: [PATCH 09/14] chore: handle loading state better --- .../ValidatorBasicInfoCard.tsx | 52 ++++++++++++++----- .../[validatorAddress]/ValueSkeleton.tsx | 11 ++++ .../nomination/[validatorAddress]/loading.tsx | 11 ++-- .../ValidatorDetails/useValidatorBasicInfo.ts | 42 +++++++++------ 4 files changed, 80 insertions(+), 36 deletions(-) create mode 100644 apps/tangle-dapp/app/nomination/[validatorAddress]/ValueSkeleton.tsx diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx index 7d729d80a9..a644cb099b 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx @@ -15,6 +15,7 @@ import { twMerge } from 'tailwind-merge'; import { SocialChip, TangleCard } from '../../../components'; import useNetworkStore from '../../../context/useNetworkStore'; import useValidatorBasicInfo from '../../../data/ValidatorDetails/useValidatorBasicInfo'; +import ValueSkeleton from './ValueSkeleton'; interface ValidatorBasicInfoCardProps { validatorAddress: string; @@ -36,6 +37,7 @@ const ValidatorBasicInfoCard: FC = ({ twitter, email, web, + isLoading, } = useValidatorBasicInfo(validatorAddress); return ( @@ -49,17 +51,25 @@ const ValidatorBasicInfoCard: FC = ({ size="lg" className="w-9 h-9" /> + + {/* Name && Active/Waiting */}
- {name && ( + {isLoading ? ( + + ) : ( - {name} + {name ?? shortenString(validatorAddress)} )} - - {isActive ? 'Active' : 'Waiting'} - + {isActive !== null && !isLoading && ( + + {isActive ? 'Active' : 'Waiting'} + + )}
+ + {/* Address */}
= ({
- {/* Restake & Nomination Info */}
+ {/* Restaked */}
Total Restaked
- - {totalRestaked ?? '--'} {nativeTokenSymbol} - - {restakingMethod?.value ?? 'N/A'} + {isLoading ? ( + + ) : ( + + {totalRestaked ?? '--'} {nativeTokenSymbol} + + )} + {!isLoading && ( + {restakingMethod?.value ?? 'N/A'} + )}
+ + {/* Nominations */}
Nominations - - {nominations ?? '--'} - + {isLoading ? ( + + ) : ( + + {nominations ?? '--'} + + )}
diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValueSkeleton.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValueSkeleton.tsx new file mode 100644 index 0000000000..e90e43af6a --- /dev/null +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValueSkeleton.tsx @@ -0,0 +1,11 @@ +import SkeletonLoader from '@webb-tools/webb-ui-components/components/SkeletonLoader'; +import { FC } from 'react'; +import { twMerge } from 'tailwind-merge'; + +const ValueSkeleton: FC<{ className?: string }> = ({ className }) => { + return ( + + ); +}; + +export default ValueSkeleton; diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx index f8ee07a4f0..e0f3464822 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx @@ -2,6 +2,7 @@ import { Spinner } from '@webb-tools/icons'; import { SkeletonLoader, Typography } from '@webb-tools/webb-ui-components'; import { GlassCard, TangleCard } from '../../../components'; +import ValueSkeleton from './ValueSkeleton'; export default function Loading() { return ( @@ -12,10 +13,8 @@ export default function Loading() {
- -
- -
+ +
@@ -24,13 +23,13 @@ export default function Loading() { Total Restaked - +
Nominations - +
diff --git a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts index 5fa5671ca4..aaf63d9eef 100644 --- a/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts +++ b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts @@ -22,6 +22,8 @@ export default function useValidatorBasicInfo(validatorAddress: string) { const [web, setWeb] = useState(null); const [twitter, setTwitter] = useState(null); const [nominations, setNominations] = useState(null); + const [isActive, setIsActive] = useState(null); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchData = async () => { @@ -52,26 +54,31 @@ export default function useValidatorBasicInfo(validatorAddress: string) { }; const fetchNominations = async () => { - if ( - currentEra !== null && - api.query.staking?.erasStakersOverview !== undefined - ) { - const erasStakersOverviewData = - await api.query.staking.erasStakersOverview( - currentEra, - validatorAddress - ); - if (erasStakersOverviewData.isSome) { - const nominatorCount = - erasStakersOverviewData.unwrap().nominatorCount; - setNominations(nominatorCount.toNumber()); - } - } else { - setNominations(0); + if (currentEra === null || !api.query.staking?.erasStakersOverview) { + setNominations(null); + setIsActive(null); + return; } + + const erasStakersOverviewData = + await api.query.staking.erasStakersOverview( + currentEra, + validatorAddress + ); + if (erasStakersOverviewData.isSome) { + const nominatorCount = + erasStakersOverviewData.unwrap().nominatorCount; + setNominations(nominatorCount.toNumber()); + setIsActive(true); + return; + } + + setNominations(null); + setIsActive(false); }; await Promise.all([fetchNameAndSocials(), fetchNominations()]); + setIsLoading(false); }; fetchData(); @@ -79,12 +86,13 @@ export default function useValidatorBasicInfo(validatorAddress: string) { return { name, - isActive: true, + isActive, totalRestaked, restakingMethod: profileTypeOpt, nominations, twitter, email, web, + isLoading, }; } From 43564fb8a0a3ab5106ee7cd4f2045b39fcbffe38 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Thu, 18 Apr 2024 11:18:16 +0700 Subject: [PATCH 10/14] chore: update number to BN with values in useRestakingProfile --- .../ValidatorBasicInfoCard.tsx | 5 ++++- .../app/restake/OverviewCard/index.tsx | 12 +++++------ apps/tangle-dapp/app/restake/page.tsx | 21 ++++++++++++------- .../data/restaking/useRestakingEarnings.ts | 6 ++++-- .../data/restaking/useRestakingProfile.ts | 11 ++++++---- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx index a644cb099b..e623e5c7ca 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx @@ -15,6 +15,7 @@ import { twMerge } from 'tailwind-merge'; import { SocialChip, TangleCard } from '../../../components'; import useNetworkStore from '../../../context/useNetworkStore'; import useValidatorBasicInfo from '../../../data/ValidatorDetails/useValidatorBasicInfo'; +import { formatTokenBalance } from '../../../utils/polkadot'; import ValueSkeleton from './ValueSkeleton'; interface ValidatorBasicInfoCardProps { @@ -110,7 +111,9 @@ const ValidatorBasicInfoCard: FC = ({ fw="bold" className="whitespace-nowrap" > - {totalRestaked ?? '--'} {nativeTokenSymbol} + {totalRestaked + ? formatTokenBalance(totalRestaked, nativeTokenSymbol) + : '--'}
)} {!isLoading && ( diff --git a/apps/tangle-dapp/app/restake/OverviewCard/index.tsx b/apps/tangle-dapp/app/restake/OverviewCard/index.tsx index c0bbfd22a7..089b7c5b58 100644 --- a/apps/tangle-dapp/app/restake/OverviewCard/index.tsx +++ b/apps/tangle-dapp/app/restake/OverviewCard/index.tsx @@ -1,6 +1,6 @@ 'use client'; -import { formatBalance } from '@polkadot/util'; +import { BN, formatBalance } from '@polkadot/util'; import SkeletonLoader from '@webb-tools/webb-ui-components/components/SkeletonLoader'; import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; import { type ComponentProps, type ElementRef, FC, forwardRef } from 'react'; @@ -16,9 +16,9 @@ type OverviewCardProps = ComponentProps<'div'> & { hasExistingProfile: boolean | null; profileTypeOpt: Optional | null; isLoading?: boolean; - totalRestaked?: number | null; - availableForRestake?: number | null; - earnings?: number | null; + totalRestaked?: BN | null; + availableForRestake?: BN | null; + earnings?: BN | null; apy?: number | null; }; @@ -83,7 +83,7 @@ export default OverviewCard; type StatsItemProps = { title: string; titleTooltip?: string; - value: number | null | undefined; + value: BN | number | null | undefined; valueTooltip?: string; isBoldText?: boolean; isLoading?: boolean; @@ -123,7 +123,7 @@ const StatsItem: FC = ({ fw={isBoldText ? 'bold' : 'normal'} className="text-mono-200 dark:text-mono-0" > - {typeof value === 'string' || typeof value === 'number' + {value != null ? formatBalance(value, { withUnit: suffix, }) diff --git a/apps/tangle-dapp/app/restake/page.tsx b/apps/tangle-dapp/app/restake/page.tsx index 1a1ddd69d7..db2024e7fc 100644 --- a/apps/tangle-dapp/app/restake/page.tsx +++ b/apps/tangle-dapp/app/restake/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import { BN, BN_ZERO } from '@polkadot/util'; import { useMemo } from 'react'; import { formatUnits } from 'viem'; @@ -31,23 +32,27 @@ const RestakePage = () => { const earnings = useMemo(() => { if (isEarningsLoading || !earningsRecord) return null; - return Object.values(earningsRecord).reduce((prev, curr) => prev + curr, 0); + return Object.values(earningsRecord).reduce( + (total, curr) => total.add(curr), + BN_ZERO + ); }, [earningsRecord, isEarningsLoading]); const availableForRestake = useMemo(() => { const fmtMaxRestakingAmount = maxRestakingAmount !== null - ? +formatUnits( - BigInt(maxRestakingAmount.toString()), - TANGLE_TOKEN_DECIMALS + ? new BN( + formatUnits( + BigInt(maxRestakingAmount.toString()), + TANGLE_TOKEN_DECIMALS + ) ) : null; if (fmtMaxRestakingAmount !== null && totalRestaked !== null) { - const availableForRestake = - fmtMaxRestakingAmount > totalRestaked - ? fmtMaxRestakingAmount - totalRestaked - : 0; + const availableForRestake = fmtMaxRestakingAmount.gt(totalRestaked) + ? fmtMaxRestakingAmount.sub(totalRestaked) + : BN_ZERO; return availableForRestake; } diff --git a/apps/tangle-dapp/data/restaking/useRestakingEarnings.ts b/apps/tangle-dapp/data/restaking/useRestakingEarnings.ts index 281925e171..372fa9d793 100644 --- a/apps/tangle-dapp/data/restaking/useRestakingEarnings.ts +++ b/apps/tangle-dapp/data/restaking/useRestakingEarnings.ts @@ -1,3 +1,4 @@ +import { BN } from '@polkadot/util'; import { useCallback } from 'react'; import { map, of } from 'rxjs'; @@ -7,7 +8,7 @@ import usePolkadotApiRx from '../../hooks/usePolkadotApiRx'; * Type for the restaking earnings record, * key is the era number and value is the restaking earnings for that era */ -export type EarningRecord = Record; +export type EarningRecord = Record; /** * Hook to get the restaking earnings for a given account @@ -28,7 +29,8 @@ const useRestakingEarnings = (substrateAccount: string | null) => return entries.reduce((prev, [era, eraRewardPoints]) => { eraRewardPoints.individual.forEach((reward, accountId32) => { if (accountId32.toString() === substrateAccount) { - prev[era.args[0].toNumber()] = reward.toNumber(); + // era is in type u32, which can be converted to number + prev[era.args[0].toNumber()] = reward; } }); diff --git a/apps/tangle-dapp/data/restaking/useRestakingProfile.ts b/apps/tangle-dapp/data/restaking/useRestakingProfile.ts index f083f805c4..5987f66cde 100644 --- a/apps/tangle-dapp/data/restaking/useRestakingProfile.ts +++ b/apps/tangle-dapp/data/restaking/useRestakingProfile.ts @@ -1,3 +1,4 @@ +import { BN } from '@polkadot/util'; import { useMemo } from 'react'; import { formatUnits } from 'viem'; @@ -35,11 +36,13 @@ const useRestakingProfile = (address?: string) => { ? // Dummy check to whether format the total restaked amount // or not, as the local testnet is in wei but the live one is in unit ledgerOpt.unwrap().total.toString().length > 10 - ? +formatUnits( - ledgerOpt.unwrap().total.toBigInt(), - TANGLE_TOKEN_DECIMALS + ? new BN( + formatUnits( + ledgerOpt.unwrap().total.toBigInt(), + TANGLE_TOKEN_DECIMALS + ) ) - : ledgerOpt.unwrap().total.toNumber() + : ledgerOpt.unwrap().total.toBn() : null, [ledgerOpt] ); From db96b744c03224f9b78ca2ef51173b461f3f91ba Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Thu, 18 Apr 2024 13:04:10 +0700 Subject: [PATCH 11/14] chore: fix chart error with BN --- apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx | 5 ++++- apps/tangle-dapp/components/charts/RoleEarningsChart.tsx | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx b/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx index 1989440825..fabeae339a 100644 --- a/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx +++ b/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx @@ -17,10 +17,13 @@ const RolesEarningsCard: FC = ({ earnings }) => { return Object.entries(earnings).map(([era, reward]) => ({ era: +era, - reward, + // Recharts can only handle number, temporarily convert to number + reward: reward.toNumber(), })); }, [earnings]); + console.log('data: ', data); + return ( diff --git a/apps/tangle-dapp/components/charts/RoleEarningsChart.tsx b/apps/tangle-dapp/components/charts/RoleEarningsChart.tsx index 2a8f90e536..06ec04bc60 100644 --- a/apps/tangle-dapp/components/charts/RoleEarningsChart.tsx +++ b/apps/tangle-dapp/components/charts/RoleEarningsChart.tsx @@ -5,7 +5,7 @@ import { Typography, useNextDarkMode, } from '@webb-tools/webb-ui-components'; -import { FC } from 'react'; +import { FC, useMemo } from 'react'; import { Bar, BarChart, @@ -24,7 +24,7 @@ const RoleEarningsChart: FC = ({ data }) => { const [isDarkMode] = useNextDarkMode(); const { nativeTokenSymbol } = useNetworkStore(); - const isEmptyData = data.length === 0; + const isEmptyData = useMemo(() => data.length === 0, [data]); return (
From 981d8bdd011bd038b0855ea0bf1ce0817980c065 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Thu, 18 Apr 2024 13:08:35 +0700 Subject: [PATCH 12/14] chore: remove console log --- apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx b/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx index fabeae339a..2925dd4b72 100644 --- a/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx +++ b/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx @@ -22,8 +22,6 @@ const RolesEarningsCard: FC = ({ earnings }) => { })); }, [earnings]); - console.log('data: ', data); - return ( From 1fd1e31086a83bb8ed4efc00212bd206c1cf2d10 Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Thu, 18 Apr 2024 21:07:51 +0700 Subject: [PATCH 13/14] chore: update table styling --- .lycheeignore | 2 ++ apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.lycheeignore b/.lycheeignore index cbcc4c1480..604965e7dd 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -37,3 +37,5 @@ https://github.com/webb-tools/webb-dapp/releases/tag/v1.0.10 https://github.com/webb-tools/webb-dapp/releases/tag/v1.0.11 https://github.com/webb-tools/webb-dapp/releases/tag/v1.0.12 https://github.com/webb-tools/webb-dapp/releases/tag/v1.0.13 +# Something happened with conventional commits link, temporary disabled to fix the CI +https://www.conventionalcommits.org/en/v1.0.0/ \ No newline at end of file diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx index 8f7be539e3..3bc3c9db82 100644 --- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx +++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx @@ -65,7 +65,7 @@ const staticColumns = [ cell: (props) => (
- +
), From de76339953b1035b8e35c7a8f1686f9e7d1167ab Mon Sep 17 00:00:00 2001 From: vutuanlinh2k2 Date: Thu, 18 Apr 2024 21:10:57 +0700 Subject: [PATCH 14/14] chore: update table styling --- apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx index 3bc3c9db82..b1f7af8707 100644 --- a/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx +++ b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx @@ -65,7 +65,9 @@ const staticColumns = [ cell: (props) => (
- +
),