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/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]/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 new file mode 100644 index 0000000000..e623e5c7ca --- /dev/null +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorBasicInfoCard.tsx @@ -0,0 +1,154 @@ +'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'; +import { FC } from 'react'; +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 { + validatorAddress: string; + className?: string; +} + +const ValidatorBasicInfoCard: FC = ({ + validatorAddress, + className, +}: ValidatorBasicInfoCardProps) => { + const { network, nativeTokenSymbol } = useNetworkStore(); + + const { + name, + isActive, + totalRestaked, + restakingMethod, + nominations, + twitter, + email, + web, + isLoading, + } = useValidatorBasicInfo(validatorAddress); + + return ( + +
+
+ + + {/* Name && Active/Waiting */} +
+
+ {isLoading ? ( + + ) : ( + + {name ?? shortenString(validatorAddress)} + + )} + {isActive !== null && !isLoading && ( + + {isActive ? 'Active' : 'Waiting'} + + )} +
+ + {/* Address */} +
+ {`Address: ${shortenString(validatorAddress, 7)}`} + + + + +
+
+
+ +
+ {/* Restaked */} +
+ + Total Restaked + +
+ {isLoading ? ( + + ) : ( + + {totalRestaked + ? formatTokenBalance(totalRestaked, nativeTokenSymbol) + : '--'} + + )} + {!isLoading && ( + {restakingMethod?.value ?? 'N/A'} + )} +
+
+ + {/* Nominations */} +
+ + Nominations + + {isLoading ? ( + + ) : ( + + {nominations ?? '--'} + + )} +
+
+ + {/* Socials & Location */} +
+
+ {twitter && } + {email && } + {web && } +
+ {/* TODO: get location later */} +
+
+
+ ); +}; + +export default ValidatorBasicInfoCard; diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorOverviewCard.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorOverviewCard.tsx deleted file mode 100644 index 01c3d2029e..0000000000 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/ValidatorOverviewCard.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { MapPinLine } from '@webb-tools/icons'; -import { - Avatar, - Chip, - CopyWithTooltip, - Typography, -} from '@webb-tools/webb-ui-components'; -import { shortenString } from '@webb-tools/webb-ui-components/utils/shortenString'; -import { twMerge } from 'tailwind-merge'; - -import { SocialChip, TangleBigLogo } from '../../../components'; -import getValidatorOverviewData from '../../../data/getValidatorOverviewData'; -import TotalRestaked from './TotalRestaked'; - -interface ValidatorOverviewCardProps { - validatorAddress: string; - className?: string; -} - -export default async function ValidatorOverviewCard({ - validatorAddress, - className, -}: ValidatorOverviewCardProps) { - const { - identity, - isActive, - totalRestaked, - restakingMethod, - nominations, - twitter, - discord, - email, - web, - location, - } = await getValidatorOverviewData(validatorAddress); - - return ( -
-
-
- -
-
- {identity && ( - - {identity} - - )} - - {isActive ? 'Active' : 'Waiting'} - -
-
- {`Address: ${shortenString(validatorAddress, 7)}`} - -
-
-
- - {/* Restake & Nomination Info */} -
-
- - Total Restaked - -
- - {restakingMethod ?? 'N/A'} -
-
-
- - Nominations - - - {nominations} - -
-
- - {/* Socials & Location */} -
-
- {twitter && } - {discord && } - {email && } - {web && } -
-
- {location && ( - - - - {location} - - - )} -
-
-
- - -
- ); -} 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 new file mode 100644 index 0000000000..e0f3464822 --- /dev/null +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/loading.tsx @@ -0,0 +1,67 @@ +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 ( +
+
+ +
+
+
+
+ + +
+
+ +
+
+ + Total Restaked + + +
+
+ + Nominations + + +
+
+
+ + + + Role Distribution + +
+ +
+
+
+ +
+ + Node Specifications + + +
+ +
+
+ + Active Services + + + Past Services + +
+ +
+
+ ); +} diff --git a/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx b/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx index 4ef75ad2ad..2a55c31949 100644 --- a/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx +++ b/apps/tangle-dapp/app/nomination/[validatorAddress]/page.tsx @@ -4,7 +4,9 @@ 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 export default function ValidatorDetails({ params, @@ -20,7 +22,7 @@ export default function ValidatorDetails({ return (
- 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/RolesEarningsCard/index.tsx b/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx index 1989440825..2925dd4b72 100644 --- a/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx +++ b/apps/tangle-dapp/app/restake/RolesEarningsCard/index.tsx @@ -17,7 +17,8 @@ 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]); diff --git a/apps/tangle-dapp/app/restake/page.tsx b/apps/tangle-dapp/app/restake/page.tsx index 8d258a3e46..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'; @@ -18,6 +19,7 @@ const RestakePage = () => { hasExistingProfile, profileTypeOpt: substrateProfileTypeOpt, ledgerOpt, + totalRestaked, } = useRestakingProfile(); const { maxRestakingAmount } = useRestakingLimits(); @@ -30,46 +32,33 @@ 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, 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( - 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; - - return { - totalRestaked, - availableForRestake, - }; + const availableForRestake = fmtMaxRestakingAmount.gt(totalRestaked) + ? fmtMaxRestakingAmount.sub(totalRestaked) + : BN_ZERO; + + return availableForRestake; } - return { - totalRestaked, - availableForRestake: fmtMaxRestakingAmount, - }; - }, [ledgerOpt, maxRestakingAmount]); + return fmtMaxRestakingAmount; + }, [maxRestakingAmount, totalRestaked]); return (
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/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/ValidatorTable/ValidatorTable.tsx b/apps/tangle-dapp/components/ValidatorTable/ValidatorTable.tsx index 5c79c3a5aa..b1f7af8707 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,20 +59,70 @@ 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) => { - 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({ @@ -120,10 +148,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; + } +} 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/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 (
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'; 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/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..aaf63d9eef --- /dev/null +++ b/apps/tangle-dapp/data/ValidatorDetails/useValidatorBasicInfo.ts @@ -0,0 +1,98 @@ +'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'; +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); + const [isActive, setIsActive] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + const api = await getPolkadotApiPromise(rpcEndpoint); + 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)); + const twitterName = extractDataFromIdentityInfo( + info, + IdentityDataType.TWITTER + ); + setTwitter( + twitterName === null + ? null + : `https://twitter.com/${twitterName.substring(1)}` + ); + } + } + }; + + const fetchNominations = async () => { + 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(); + }, [validatorAddress, rpcEndpoint, currentEra]); + + return { + name, + isActive, + totalRestaked, + restakingMethod: profileTypeOpt, + nominations, + twitter, + email, + web, + isLoading, + }; +} 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/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 9a202ba7a6..5987f66cde 100644 --- a/apps/tangle-dapp/data/restaking/useRestakingProfile.ts +++ b/apps/tangle-dapp/data/restaking/useRestakingProfile.ts @@ -1,11 +1,14 @@ +import { BN } from '@polkadot/util'; 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 +30,28 @@ 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 + ? new BN( + formatUnits( + ledgerOpt.unwrap().total.toBigInt(), + TANGLE_TOKEN_DECIMALS + ) + ) + : ledgerOpt.unwrap().total.toBn() + : 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] ) ); }; 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; 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: () => , +};