From f5986b68604705fe3b3f1503df2bdf9705ab122c Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Wed, 18 Sep 2024 12:37:35 +0200 Subject: [PATCH] feat(devx): Add filter flag to Iota CLI Signed-off-by: salaheldinsoliman --- .changeset/config.json | 3 +- .changeset/ten-vans-cross.md | 6 - Cargo.lock | 1 + .../src/features/features.mock.ts | 113 ++++++- apps/core/src/api/SentryHttpTransport.ts | 2 +- apps/core/src/hooks/index.ts | 1 + apps/core/src/hooks/nameService.ts | 57 ++++ .../src/hooks/useIotaAddressValidation.ts | 9 +- .../createNftSendValidationSchema.ts | 10 +- .../validation/createIotaAdressValidation.ts | 20 +- .../src/components/AddressesCardGraph.tsx | 3 + .../src/components/TransactionsCardGraph.tsx | 2 + .../components/gas-breakdown/GasBreakdown.tsx | 10 +- .../components/home-metrics/CurrentEpoch.tsx | 8 +- .../components/home-metrics/OnTheNetwork.tsx | 10 +- .../src/components/owned-coins/CoinIcon.tsx | 9 +- .../src/components/owned-coins/CoinItem.tsx | 26 +- .../components/owned-coins/OwnedCoinView.tsx | 100 ++++-- .../src/components/owned-coins/OwnedCoins.tsx | 235 ++++++-------- .../owned-coins/OwnedCoinsPanel.tsx | 6 +- .../src/components/owned-objects/ListView.tsx | 2 +- .../owned-objects/SmallThumbnailsView.tsx | 35 +- .../owned-objects/ThumbnailsView.tsx | 13 +- .../top-validators-card/TopValidatorsCard.tsx | 138 ++++++-- apps/explorer/src/components/ui/ImageIcon.tsx | 4 +- .../src/components/ui/InternalLink.tsx | 10 +- .../src/components/ui/ObjectVideoImage.tsx | 3 +- .../explorer/src/components/ui/Pagination.tsx | 54 +++- apps/explorer/src/components/ui/RingChart.tsx | 85 ++--- apps/explorer/src/components/ui/TableCard.tsx | 23 +- .../src/components/ui/image/Image.tsx | 10 +- .../components/validator/ValidatorStats.tsx | 11 + apps/explorer/src/hooks/useSearch.ts | 22 +- .../ui/utils/generateValidatorsTableData.tsx | 302 ------------------ apps/explorer/src/lib/ui/utils/index.ts | 1 - .../pages/address-result/AddressResult.tsx | 179 +++++++---- .../pages/checkpoints/CheckpointDetail.tsx | 7 + .../explorer/src/pages/epochs/EpochDetail.tsx | 180 +++++------ .../src/pages/epochs/stats/EpochProgress.tsx | 68 ++++ .../src/pages/epochs/stats/EpochStats.tsx | 35 +- .../src/pages/epochs/stats/EpochTopStats.tsx | 48 --- .../src/pages/epochs/stats/TokenStats.tsx | 30 -- .../pages/epochs/stats/ValidatorStatus.tsx | 63 ++-- apps/explorer/src/pages/id-page/index.tsx | 39 ++- .../pages/object-result/views/ObjectView.tsx | 1 - .../transaction-summary/BalanceChanges.tsx | 4 +- .../transaction-summary/ObjectChanges.tsx | 7 +- .../TransactionDetails.tsx | 6 +- .../src/pages/validators/Validators.tsx | 239 +++++++++++++- .../components/atoms/label-text/LabelText.tsx | 11 +- .../components/atoms/list-item/ListItem.tsx | 15 +- .../lib/components/molecules/chip/Chip.tsx | 29 +- .../molecules/display-stats/DisplayStats.tsx | 2 +- .../dropdown/dropdown-position.enum.ts | 7 - .../components/molecules/dropdown/index.ts | 1 - .../components/molecules/select/Select.tsx | 11 +- .../molecules/table-cell/TableCell.tsx | 48 +-- .../molecules/table-cell/table-cell.enums.ts | 6 - .../table-header-cell/TableHeaderCell.tsx | 8 +- apps/ui-kit/src/lib/constants/index.ts | 4 - apps/ui-kit/src/lib/index.ts | 1 - apps/ui-kit/src/lib/tailwind/base.preset.ts | 2 +- .../constants/colors.constants.ts | 0 .../src/lib/tailwind/constants/index.ts | 1 + .../stories/atoms/LabelText.stories.tsx | 4 + .../stories/design-tokens/colors.stories.mdx | 2 +- .../stories/molecules/Chip.stories.tsx | 2 +- apps/wallet-dashboard/package.json | 2 +- apps/wallet/src/shared/utils/url.ts | 2 +- .../app/components/accounts/AccountItem.tsx | 4 +- .../accounts/AccountItemApproveConnection.tsx | 6 +- .../components/ledger/LedgerAccountList.tsx | 5 +- .../wallet/src/ui/app/hooks/useAddressLink.ts | 7 +- .../accounts/manage/AccountGroupItem.tsx | 4 +- .../home/nft-transfer/TransferNFTForm.tsx | 27 +- .../home/transfer-coin/SendTokenForm.tsx | 30 +- .../pages/home/transfer-coin/validation.ts | 9 +- .../page-main-layout/PageMainLayout.tsx | 4 +- crates/iota-json-rpc-types/Cargo.toml | 2 + .../src/iota_transaction.rs | 40 ++- crates/iota-sdk/src/wallet_context.rs | 11 +- .../iota-test-transaction-builder/src/lib.rs | 14 +- crates/iota/src/client_commands.rs | 46 ++- crates/iota/src/client_ptb/ptb.rs | 4 +- crates/iota/tests/cli_tests.rs | 15 + crates/test-cluster/src/lib.rs | 6 +- docker/fullnode/README.md | 16 +- .../execution-architecture/adapter.mdx | 0 .../execution-layer.mdx | 2 +- .../execution-architecture/iota-execution.mdx | 0 .../execution-architecture/natives.mdx | 0 .../iota-architecture/iota-architecture.mdx | 2 +- .../developer/cryptography/on-chain/ecvrf.mdx | 2 +- .../cryptography/on-chain/hashing.mdx | 6 +- docs/content/developer/developer.mdx | 47 ++- .../developer/evm-to-move/creating-token.mdx | 5 +- .../developer/getting-started/connect.mdx | 12 +- .../getting-started/create-a-package.mdx | 16 +- .../developer/getting-started/get-address.mdx | 2 +- .../getting-started/iota-install.mdx | 8 +- .../getting-started/local-network.mdx | 6 +- docs/content/operator/iota-full-node.mdx | 68 ++-- docs/content/references/cli/console.mdx | 4 +- .../contribute/contribute-to-iota-repos.mdx | 5 + .../contribute/localize-iota-docs.mdx | 6 + docs/content/references/iota-glossary.mdx | 6 +- .../references/ts-sdk/dapp-kit/rpc-hooks.mdx | 22 ++ docs/content/sidebars/about-iota.js | 19 ++ docs/content/sidebars/references.js | 26 +- nre/validator_tool.md | 2 - pnpm-lock.yaml | 128 +++++--- .../src/components/AccountDropdownMenu.tsx | 13 +- .../src/hooks/useResolveIotaNSNames.ts | 31 ++ sdk/dapp-kit/src/index.ts | 1 + sdk/iotans-toolkit/README.md | 81 +++++ sdk/iotans-toolkit/package.json | 49 +++ sdk/iotans-toolkit/src/client.ts | 199 ++++++++++++ sdk/iotans-toolkit/src/index.ts | 7 + sdk/iotans-toolkit/src/types/index.ts | 5 + sdk/iotans-toolkit/src/types/objects.ts | 22 ++ sdk/iotans-toolkit/src/utils/constants.ts | 7 + sdk/iotans-toolkit/src/utils/parser.ts | 43 +++ sdk/iotans-toolkit/src/utils/queries.ts | 33 ++ sdk/iotans-toolkit/tests/app.test.ts | 90 ++++++ sdk/iotans-toolkit/tsconfig.esm.json | 7 + sdk/iotans-toolkit/tsconfig.json | 11 + sdk/iotans-toolkit/vitest.config.ts | 24 ++ sdk/typescript/src/client/client.ts | 13 + sdk/typescript/src/client/types/chain.ts | 6 + 129 files changed, 2389 insertions(+), 1305 deletions(-) delete mode 100644 .changeset/ten-vans-cross.md create mode 100644 apps/core/src/hooks/nameService.ts delete mode 100644 apps/explorer/src/lib/ui/utils/generateValidatorsTableData.tsx create mode 100644 apps/explorer/src/pages/epochs/stats/EpochProgress.tsx delete mode 100644 apps/explorer/src/pages/epochs/stats/EpochTopStats.tsx delete mode 100644 apps/explorer/src/pages/epochs/stats/TokenStats.tsx delete mode 100644 apps/ui-kit/src/lib/components/molecules/dropdown/dropdown-position.enum.ts delete mode 100644 apps/ui-kit/src/lib/constants/index.ts rename apps/ui-kit/src/lib/{ => tailwind}/constants/colors.constants.ts (100%) rename docs/content/{references => about-iota}/execution-architecture/adapter.mdx (100%) rename docs/content/{references => about-iota}/execution-architecture/execution-layer.mdx (87%) rename docs/content/{references => about-iota}/execution-architecture/iota-execution.mdx (100%) rename docs/content/{references => about-iota}/execution-architecture/natives.mdx (100%) create mode 100644 docs/content/references/contribute/localize-iota-docs.mdx create mode 100644 sdk/dapp-kit/src/hooks/useResolveIotaNSNames.ts create mode 100644 sdk/iotans-toolkit/README.md create mode 100644 sdk/iotans-toolkit/package.json create mode 100644 sdk/iotans-toolkit/src/client.ts create mode 100644 sdk/iotans-toolkit/src/index.ts create mode 100644 sdk/iotans-toolkit/src/types/index.ts create mode 100644 sdk/iotans-toolkit/src/types/objects.ts create mode 100644 sdk/iotans-toolkit/src/utils/constants.ts create mode 100644 sdk/iotans-toolkit/src/utils/parser.ts create mode 100644 sdk/iotans-toolkit/src/utils/queries.ts create mode 100644 sdk/iotans-toolkit/tests/app.test.ts create mode 100644 sdk/iotans-toolkit/tsconfig.esm.json create mode 100644 sdk/iotans-toolkit/tsconfig.json create mode 100644 sdk/iotans-toolkit/vitest.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index f77b2cb71c4..c6135480308 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -21,7 +21,8 @@ "sponsored-transactions", "kiosk-demo", "kiosk-cli", - "@iota/zklogin" + "@iota/zklogin", + "@iota/iotans-toolkit" ], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true diff --git a/.changeset/ten-vans-cross.md b/.changeset/ten-vans-cross.md deleted file mode 100644 index fcf32386432..00000000000 --- a/.changeset/ten-vans-cross.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@iota/iota-sdk': minor -'@iota/dapp-kit': minor ---- - -Deprecate IOTA Name Service diff --git a/Cargo.lock b/Cargo.lock index 0588e0451a2..0e7cef84098 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6176,6 +6176,7 @@ version = "0.0.0" dependencies = [ "anyhow", "bcs", + "clap", "colored", "enum_dispatch", "fastcrypto", diff --git a/apps/apps-backend/src/features/features.mock.ts b/apps/apps-backend/src/features/features.mock.ts index 8a4a93b8c70..f45172291bf 100644 --- a/apps/apps-backend/src/features/features.mock.ts +++ b/apps/apps-backend/src/features/features.mock.ts @@ -11,6 +11,13 @@ const walletDapps = [ icon: 'https://iotafrens.com/icons/favicon.ico', tags: ['Social'], }, + { + name: 'Iota Name Service (IotaNS)', + description: 'Find your .iota name!', + link: 'https://iotans.io/', + icon: 'https://raw.githubusercontent.com/IotaNSdapp/docs/main/IotaNS-small2.jpg', + tags: ['Infra'], + }, { name: 'Wormhole Connect', description: @@ -152,6 +159,9 @@ export const developmentFeatures = { '0x30a644c3485ee9b604f52165668895092191fcaf5489a846afa7fc11cdb9b24a', ], }, + iotans: { + defaultValue: false, + }, 'team-address-overrides': { defaultValue: { addresses: [ @@ -205,7 +215,15 @@ export const developmentFeatures = { defaultValue: 0.0025, }, 'wallet-dapps': { - defaultValue: [], + defaultValue: [ + { + name: 'Iota Name Service (IotaNS)', + description: 'Find your .iota name!', + link: 'https://iotans.io/', + icon: 'https://raw.githubusercontent.com/IotaNSdapp/docs/main/IotaNS-small2.jpg', + tags: ['Infra'], + }, + ], rules: [ { condition: { @@ -379,6 +397,83 @@ export const developmentFeatures = { }, ], }, + 'iotans-enable-okx-wallet': { + defaultValue: false, + rules: [ + { + condition: { + network: { + $ne: 'mainnet', + }, + }, + force: true, + }, + ], + }, + 'iotans-enable-day-one-nft-domain-claim': { + defaultValue: false, + }, + 'iotans-nft-personalization': { + defaultValue: false, + rules: [ + { + condition: { + network: { + $ne: 'mainnet', + }, + }, + force: true, + }, + ], + }, + 'iotans-front-page-banner': { + defaultValue: { + enabled: false, + dismissKey: 'quests-3-interstitial-live', + imageUrl: 'https://fe-assets.iota.org/quests_3_updated_large_corrected.svg', + bannerUrl: 'https://tech.iota.org/quest-3/', + }, + }, + 'iotans-enable-coupons': { + defaultValue: false, + }, + 'iotans-enable-discord': { + defaultValue: false, + rules: [ + { + condition: { + network: { + $ne: 'mainnet', + }, + }, + force: true, + }, + ], + }, + 'iotans-free-claims': { + defaultValue: false, + }, + 'iotans-banner': { + defaultValue: { + content: + "IotaNS is experiencing some issues. We're working to fix the problem and appreciate your patience.", + isActive: false, + isDismissable: true, + }, + }, + 'iotans-enable-subname': { + defaultValue: false, + rules: [ + { + condition: { + network: { + $ne: 'mainnet', + }, + }, + force: true, + }, + ], + }, expiration_period: { defaultValue: 30, rules: [ @@ -392,6 +487,22 @@ export const developmentFeatures = { }, ], }, + 'iotans-name-burn-expired-name': { + defaultValue: false, + rules: [ + { + force: true, + }, + ], + }, + 'iotans-name-enable-v2-design': { + defaultValue: false, + rules: [ + { + force: true, + }, + ], + }, 'validator-page-staking': { defaultValue: true, }, diff --git a/apps/core/src/api/SentryHttpTransport.ts b/apps/core/src/api/SentryHttpTransport.ts index 66fa4756c94..7ed5331c0e2 100644 --- a/apps/core/src/api/SentryHttpTransport.ts +++ b/apps/core/src/api/SentryHttpTransport.ts @@ -5,7 +5,7 @@ import { IotaHTTPTransport } from '@iota/iota-sdk/client'; import * as Sentry from '@sentry/react'; -const IGNORED_METHODS: string[] = []; +const IGNORED_METHODS = ['iotax_resolveNameServiceNames', 'iotax_resolveNameServiceAddresses']; export class SentryHttpTransport extends IotaHTTPTransport { private url: string; diff --git a/apps/core/src/hooks/index.ts b/apps/core/src/hooks/index.ts index 0219881a4d2..449122dee19 100644 --- a/apps/core/src/hooks/index.ts +++ b/apps/core/src/hooks/index.ts @@ -5,6 +5,7 @@ export * from './useFormatCoin'; export * from './useTimeAgo'; export * from './useGetValidatorsEvents'; export * from './useGetValidatorsApy'; +export * from './nameService'; export * from './useGetTransferAmount'; export * from './useGetObject'; export * from './useGetDynamicFields'; diff --git a/apps/core/src/hooks/nameService.ts b/apps/core/src/hooks/nameService.ts new file mode 100644 index 00000000000..417fa15e992 --- /dev/null +++ b/apps/core/src/hooks/nameService.ts @@ -0,0 +1,57 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useFeatureIsOn } from '@growthbook/growthbook-react'; +import { useIotaClient } from '@iota/dapp-kit'; +import { useQuery } from '@tanstack/react-query'; + +const IOTA_NS_FEATURE_FLAG = 'iotans'; + +// This should align with whatever names we want to be able to resolve. +const IOTA_NS_DOMAINS = ['.iota']; +export function isIotaNSName(name: string) { + return IOTA_NS_DOMAINS.some((domain) => name.endsWith(domain)); +} + +export function useIotaNSEnabled() { + return useFeatureIsOn(IOTA_NS_FEATURE_FLAG); +} + +export function useResolveIotaNSAddress(name?: string | null, enabled?: boolean) { + const client = useIotaClient(); + const enabledIotaNs = useIotaNSEnabled(); + + return useQuery({ + queryKey: ['resolve-iotans-address', name], + queryFn: async () => { + return await client.resolveNameServiceAddress({ + name: name!, + }); + }, + enabled: !!name && enabled && enabledIotaNs, + refetchOnWindowFocus: false, + retry: false, + }); +} + +export function useResolveIotaNSName(address?: string | null) { + const client = useIotaClient(); + const enabled = useIotaNSEnabled(); + + return useQuery({ + queryKey: ['resolve-iotans-name', address], + queryFn: async () => { + // NOTE: We only fetch 1 here because it's the default name. + const { data } = await client.resolveNameServiceNames({ + address: address!, + limit: 1, + }); + + return data[0] || null; + }, + enabled: !!address && enabled, + refetchOnWindowFocus: false, + retry: false, + }); +} diff --git a/apps/core/src/hooks/useIotaAddressValidation.ts b/apps/core/src/hooks/useIotaAddressValidation.ts index cd2c239f824..639007b4e68 100644 --- a/apps/core/src/hooks/useIotaAddressValidation.ts +++ b/apps/core/src/hooks/useIotaAddressValidation.ts @@ -1,11 +1,16 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { useIotaNSEnabled } from '.'; +import { useIotaClient } from '@iota/dapp-kit'; import { useMemo } from 'react'; import { createIotaAddressValidation } from '../utils'; export function useIotaAddressValidation() { + const client = useIotaClient(); + const iotaNSEnabled = useIotaNSEnabled(); + return useMemo(() => { - return createIotaAddressValidation(); - }, []); + return createIotaAddressValidation(client, iotaNSEnabled); + }, [client, iotaNSEnabled]); } diff --git a/apps/core/src/utils/transaction/createNftSendValidationSchema.ts b/apps/core/src/utils/transaction/createNftSendValidationSchema.ts index 0ff4c23bf07..ba41c8b87ed 100644 --- a/apps/core/src/utils/transaction/createNftSendValidationSchema.ts +++ b/apps/core/src/utils/transaction/createNftSendValidationSchema.ts @@ -2,13 +2,19 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { type IotaClient } from '@iota/iota-sdk/client'; import * as Yup from 'yup'; import { createIotaAddressValidation } from '../validation'; import { ValidationError } from 'yup'; -export function createNftSendValidationSchema(senderAddress: string, objectId: string) { +export function createNftSendValidationSchema( + senderAddress: string, + objectId: string, + client?: IotaClient, + iotaNSEnabled?: boolean, +) { return Yup.object({ - to: createIotaAddressValidation() + to: createIotaAddressValidation(client, iotaNSEnabled) .test( 'sender-address', 'NFT is owned by this address', diff --git a/apps/core/src/utils/validation/createIotaAdressValidation.ts b/apps/core/src/utils/validation/createIotaAdressValidation.ts index 0e89684408a..bb49fe71aca 100644 --- a/apps/core/src/utils/validation/createIotaAdressValidation.ts +++ b/apps/core/src/utils/validation/createIotaAdressValidation.ts @@ -2,18 +2,36 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { IotaClient } from '@iota/iota-sdk/client'; +import { isIotaNSName } from '../../hooks'; import { isValidIotaAddress } from '@iota/iota-sdk/utils'; import * as Yup from 'yup'; import { ValidationError } from 'yup'; export { ValidationError }; -export function createIotaAddressValidation() { +export function createIotaAddressValidation(client?: IotaClient, iotaNSEnabled?: boolean) { + const resolveCache = new Map(); + return Yup.string() .ensure() .trim() .required() .test('is-iota-address', 'Invalid address. Please check again.', async (value) => { + if (client && iotaNSEnabled && isIotaNSName(value)) { + if (resolveCache.has(value)) { + return resolveCache.get(value)!; + } + + const address = await client.resolveNameServiceAddress({ + name: value, + }); + + resolveCache.set(value, !!address); + + return !!address; + } + return isValidIotaAddress(value); }) .label("Recipient's address"); diff --git a/apps/explorer/src/components/AddressesCardGraph.tsx b/apps/explorer/src/components/AddressesCardGraph.tsx index 4f6b0d6cc1c..a008e55e007 100644 --- a/apps/explorer/src/components/AddressesCardGraph.tsx +++ b/apps/explorer/src/components/AddressesCardGraph.tsx @@ -55,6 +55,7 @@ export function AddressesCardGraph(): JSX.Element { ? addressMetrics.cumulativeAddresses.toString() : '--' } + showSupportingLabel={false} /> @@ -67,6 +68,7 @@ export function AddressesCardGraph(): JSX.Element { ? addressMetrics.cumulativeActiveAddresses.toString() : '--' } + showSupportingLabel={false} /> @@ -78,6 +80,7 @@ export function AddressesCardGraph(): JSX.Element { ? addressMetrics.dailyActiveAddresses.toString() : '--' } + showSupportingLabel={false} />
{isPending ? ( diff --git a/apps/explorer/src/components/TransactionsCardGraph.tsx b/apps/explorer/src/components/TransactionsCardGraph.tsx index 1273c6f8c0e..72f3f647291 100644 --- a/apps/explorer/src/components/TransactionsCardGraph.tsx +++ b/apps/explorer/src/components/TransactionsCardGraph.tsx @@ -85,6 +85,7 @@ export function TransactionsCardGraph() { size={LabelTextSize.Large} label="Total" text={totalTransactions ? formatBalance(totalTransactions, 0) : '--'} + showSupportingLabel={false} />
@@ -97,6 +98,7 @@ export function TransactionsCardGraph() { ? lastEpochTotalTransactions.toString() : '--' } + showSupportingLabel={false} /> diff --git a/apps/explorer/src/components/gas-breakdown/GasBreakdown.tsx b/apps/explorer/src/components/gas-breakdown/GasBreakdown.tsx index 1fb2fdbb0df..d7609ca3ccc 100644 --- a/apps/explorer/src/components/gas-breakdown/GasBreakdown.tsx +++ b/apps/explorer/src/components/gas-breakdown/GasBreakdown.tsx @@ -2,7 +2,12 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { CoinFormat, type TransactionSummary, useFormatCoin } from '@iota/core'; +import { + CoinFormat, + type TransactionSummary, + useFormatCoin, + useResolveIotaNSName, +} from '@iota/core'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { Heading, Text } from '@iota/ui'; @@ -95,6 +100,7 @@ interface GasBreakdownProps { export function GasBreakdown({ summary }: GasBreakdownProps): JSX.Element | null { const gasData = summary?.gas; + const { data: iotansDomainName } = useResolveIotaNSName(gasData?.owner); if (!gasData) { return null; @@ -126,7 +132,7 @@ export function GasBreakdown({ summary }: GasBreakdownProps): JSX.Element | null Paid by - + )} diff --git a/apps/explorer/src/components/home-metrics/CurrentEpoch.tsx b/apps/explorer/src/components/home-metrics/CurrentEpoch.tsx index 6fd692336ef..dc97efc7b0d 100644 --- a/apps/explorer/src/components/home-metrics/CurrentEpoch.tsx +++ b/apps/explorer/src/components/home-metrics/CurrentEpoch.tsx @@ -52,7 +52,12 @@ export function CurrentEpoch(): JSX.Element {
- +
diff --git a/apps/explorer/src/components/home-metrics/OnTheNetwork.tsx b/apps/explorer/src/components/home-metrics/OnTheNetwork.tsx index c2cf1bb57b6..2b7ee68e7e5 100644 --- a/apps/explorer/src/components/home-metrics/OnTheNetwork.tsx +++ b/apps/explorer/src/components/home-metrics/OnTheNetwork.tsx @@ -36,6 +36,7 @@ export function OnTheNetwork(): JSX.Element { ? Math.floor(networkMetrics.currentTps).toString() : '-' } + showSupportingLabel={false} />
@@ -48,6 +49,7 @@ export function OnTheNetwork(): JSX.Element { ? Math.floor(networkMetrics?.tps30Days).toString() : '-' } + showSupportingLabel={false} /> @@ -60,6 +62,7 @@ export function OnTheNetwork(): JSX.Element { size={LabelTextSize.Large} label="Total Packages" text={networkMetrics?.totalPackages ?? '-'} + showSupportingLabel={false} />
@@ -67,6 +70,7 @@ export function OnTheNetwork(): JSX.Element { size={LabelTextSize.Large} label="Objects" text={networkMetrics?.totalObjects ?? '-'} + showSupportingLabel={false} />
@@ -77,7 +81,8 @@ export function OnTheNetwork(): JSX.Element { size={LabelTextSize.Large} label="Reference Gas Price" text={gasPriceFormatted ?? '-'} - supportingLabel={gasPriceFormatted !== null ? 'IOTA' : undefined} + showSupportingLabel={gasPriceFormatted !== null} + supportingLabel="IOTA" />
@@ -85,7 +90,8 @@ export function OnTheNetwork(): JSX.Element { size={LabelTextSize.Large} label="Total Supply" text={totalSupplyFormatted ?? '-'} - supportingLabel={totalSupplyFormatted !== null ? 'IOTA' : undefined} + showSupportingLabel={totalSupplyFormatted !== null} + supportingLabel="IOTA" />
diff --git a/apps/explorer/src/components/owned-coins/CoinIcon.tsx b/apps/explorer/src/components/owned-coins/CoinIcon.tsx index ad4548288d5..db30e9b6b0a 100644 --- a/apps/explorer/src/components/owned-coins/CoinIcon.tsx +++ b/apps/explorer/src/components/owned-coins/CoinIcon.tsx @@ -3,8 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 import { useCoinMetadata } from '@iota/core'; -import { Unstaked } from '@iota/icons'; -import { IotaLogoMark as Iota } from '@iota/ui-icons'; +import { Iota, Unstaked } from '@iota/icons'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { cva, type VariantProps } from 'class-variance-authority'; @@ -26,7 +25,7 @@ const imageStyle = cva(['flex rounded-2xl'], { function IotaCoin(): JSX.Element { return ( - + ); } @@ -37,7 +36,7 @@ type NonIotaCoinProps = { function NonIotaCoin({ coinType }: NonIotaCoinProps): JSX.Element { const { data: coinMeta } = useCoinMetadata(coinType); return ( -
+
{coinMeta?.iconUrl ? ( ) : ( -
+
)} diff --git a/apps/explorer/src/components/owned-coins/CoinItem.tsx b/apps/explorer/src/components/owned-coins/CoinItem.tsx index abe73c73824..9f9d6217fa0 100644 --- a/apps/explorer/src/components/owned-coins/CoinItem.tsx +++ b/apps/explorer/src/components/owned-coins/CoinItem.tsx @@ -2,23 +2,29 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { KeyValueInfo } from '@iota/apps-ui-kit'; import { useFormatCoin } from '@iota/core'; import { type CoinStruct } from '@iota/iota-sdk/client'; -import { formatAddress } from '@iota/iota-sdk/utils'; +import { Text } from '@iota/ui'; -interface CoinItemProps { +import { ObjectLink } from '~/components/ui'; + +type CoinItemProps = { coin: CoinStruct; -} +}; export default function CoinItem({ coin }: CoinItemProps): JSX.Element { const [formattedBalance, symbol] = useFormatCoin(coin.balance, coin.coinType); return ( - +
+ +
+ + {formattedBalance} + + + {symbol} + +
+
); } diff --git a/apps/explorer/src/components/owned-coins/OwnedCoinView.tsx b/apps/explorer/src/components/owned-coins/OwnedCoinView.tsx index 485d0738aa5..466972270a2 100644 --- a/apps/explorer/src/components/owned-coins/OwnedCoinView.tsx +++ b/apps/explorer/src/components/owned-coins/OwnedCoinView.tsx @@ -3,15 +3,18 @@ // SPDX-License-Identifier: Apache-2.0 import { useFormatCoin } from '@iota/core'; +import { ArrowShowAndHideRight12, Warning16 } from '@iota/icons'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { Text } from '@iota/ui'; +import * as Collapsible from '@radix-ui/react-collapsible'; import clsx from 'clsx'; import { useState } from 'react'; +import { Banner, Tooltip } from '~/components/ui'; +import { ampli } from '~/lib/utils'; import { CoinIcon } from './CoinIcon'; import { type CoinBalanceVerified } from './OwnedCoins'; import CoinsPanel from './OwnedCoinsPanel'; -import { Card, CardBody, Chip, Tooltip } from '@iota/apps-ui-kit'; -import { ArrowUp, Warning } from '@iota/ui-icons'; type OwnedCoinViewProps = { coin: CoinBalanceVerified; @@ -20,46 +23,77 @@ type OwnedCoinViewProps = { export default function OwnedCoinView({ coin, id }: OwnedCoinViewProps): JSX.Element { const isIotaCoin = coin.coinType === IOTA_TYPE_ARG; - const [areCoinDetailsOpen, setAreCoinDetailsOpen] = useState(isIotaCoin); + const [open, setOpen] = useState(isIotaCoin); const [formattedTotalBalance, symbol] = useFormatCoin(coin.totalBalance, coin.coinType); - const CARD_BODY: React.ComponentProps = { - title: symbol, - subtitle: `${formattedTotalBalance} ${symbol}`, - }; - - const CHIP_PROPS: React.ComponentProps = { - label: `${coin.coinObjectCount} Object` + (coin.coinObjectCount > 1 ? 's' : ''), - trailingElement: , - }; return ( -
- -
- -
-
- + + +
+ + +
+
+ +
+ + {symbol} + +
+ {!coin.isRecognized && ( - - + + ampli.activatedTooltip({ + tooltipLabel: 'unrecognizedCoinWarning', + }) + } + > + + + )}
-
- { - setAreCoinDetailsOpen((prev) => !prev); - }} - /> + +
+ + {coin.coinObjectCount} +
- - {areCoinDetailsOpen && ( -
+ +
+ + {formattedTotalBalance} + + + {symbol} + +
+ + + +
- )} -
+ + ); } diff --git a/apps/explorer/src/components/owned-coins/OwnedCoins.tsx b/apps/explorer/src/components/owned-coins/OwnedCoins.tsx index d0dab0ea297..0a9fae9fb78 100644 --- a/apps/explorer/src/components/owned-coins/OwnedCoins.tsx +++ b/apps/explorer/src/components/owned-coins/OwnedCoins.tsx @@ -4,26 +4,15 @@ import { getCoinSymbol } from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; +import { Info16 } from '@iota/icons'; import { type CoinBalance } from '@iota/iota-sdk/client'; import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; -import { LoadingIndicator } from '@iota/ui'; -import { FilterList, Warning } from '@iota/ui-icons'; +import { Heading, Text, LoadingIndicator, RadioGroup, RadioGroupItem } from '@iota/ui'; import { useMemo, useState } from 'react'; + import OwnedCoinView from './OwnedCoinView'; import { useRecognizedPackages } from '~/hooks/useRecognizedPackages'; -import { - Button, - ButtonType, - Dropdown, - DropdownPosition, - InfoBox, - InfoBoxStyle, - InfoBoxType, - ListItem, - Select, - Title, -} from '@iota/apps-ui-kit'; -import { Pagination } from '../ui'; +import { Pagination } from '~/components/ui'; export type CoinBalanceVerified = CoinBalance & { isRecognized?: boolean; @@ -41,7 +30,7 @@ interface OwnerCoinsProps { export function OwnedCoins({ id }: OwnerCoinsProps): JSX.Element { const [currentSlice, setCurrentSlice] = useState(1); const [limit, setLimit] = useState(20); - const [filterValue, setFilterValue] = useState(CoinFilter.All); + const [filterValue, setFilterValue] = useState(CoinFilter.Recognized); const { isPending, data, isError } = useIotaClientQuery('getAllBalances', { owner: normalizeIotaAddress(id), }); @@ -94,33 +83,24 @@ export function OwnedCoins({ id }: OwnerCoinsProps): JSX.Element { }; }, [data, recognizedPackages]); - const filterOptions: FilterOption[] = useMemo( + const filterOptions = useMemo( () => [ { - label: 'All', - counter: balances.allBalances.length, - onClick: () => setFilterValue(CoinFilter.All), - }, - { - label: `Recognized`, - counter: balances.recognizedBalances.length, - isDisabled: !balances.recognizedBalances.length, - onClick: () => setFilterValue(CoinFilter.Recognized), + label: `${balances.recognizedBalances.length} RECOGNIZED`, + value: CoinFilter.Recognized, }, { - label: `Unrecognized`, - counter: balances.unrecognizedBalances.length, - isDisabled: !balances.unrecognizedBalances.length, - onClick: () => setFilterValue(CoinFilter.Unrecognized), + label: `${balances.unrecognizedBalances.length} UNRECOGNIZED`, + value: CoinFilter.Unrecognized, }, + { label: 'ALL', value: CoinFilter.All }, ], [balances], ); - const hasCoinsBalance = balances.allBalances.length > 0; const displayedBalances = useMemo(() => balances[filterValue], [balances, filterValue]); - const coinBalanceHeader = - `${displayedBalances.length ?? 0} Coin` + (displayedBalances.length !== 1 ? 's' : ''); + const hasCoinsBalance = balances.allBalances.length > 0; + const coinBalanceHeader = hasCoinsBalance ? `${balances.allBalances.length} Coins` : 'Coins'; if (isError) { return ( @@ -128,40 +108,82 @@ export function OwnedCoins({ id }: OwnerCoinsProps): JSX.Element { ); } - const visibleCoins = displayedBalances.slice((currentSlice - 1) * limit, currentSlice * limit); - return ( -
+
{isPending ? (
) : ( -
- - } - /> - {hasCoinsBalance ? ( - <> - <div className="relative overflow-y-auto p-sm--rs pt-0"> - <div className="sticky top-0 z-[1] bg-neutral-100 p-sm dark:bg-neutral-10"> - {filterValue === CoinFilter.Unrecognized && ( - <InfoBox - icon={<Warning />} - supportingText="These coins have not been recognized by the Iota Foundation." - type={InfoBoxType.Default} - style={InfoBoxStyle.Default} + <div className="relative flex h-full flex-col gap-4 overflow-auto text-left"> + <div className="flex min-h-14 w-full flex-col justify-between gap-y-3 border-b border-gray-45 max-sm:pb-3 max-sm:pt-5 sm:flex-row sm:items-center"> + <Heading color="steel-darker" variant="heading4/semibold"> + {coinBalanceHeader} + </Heading> + {hasCoinsBalance && ( + <div> + <RadioGroup + aria-label="transaction filter" + value={filterValue} + onValueChange={(value) => setFilterValue(value as CoinFilter)} + > + {filterOptions.map((filter) => ( + <RadioGroupItem + key={filter.value} + value={filter.value} + label={filter.label} + disabled={!balances[filter.value].length} /> - )} - </div> - <CoinList coins={visibleCoins} id={id} /> + ))} + </RadioGroup> + </div> + )} + </div> + {filterValue === CoinFilter.Unrecognized && ( + <div className="flex items-center gap-2 rounded-2xl border border-gray-45 p-2 text-steel-darker"> + <div> + <Info16 width="16px" /> </div> + <Text color="steel-darker" variant="body/medium"> + These coins have not been recognized by Iota Foundation. + </Text> + </div> + )} + {hasCoinsBalance && ( + <> + <div className="flex max-h-coinsAndAssetsContainer flex-col overflow-auto md:max-h-full"> + <div className="mb-2.5 flex uppercase tracking-wider text-gray-80"> + <div className="w-[45%] pl-3"> + <Text variant="caption/medium" color="steel-dark"> + Type + </Text> + </div> + <div className="w-[25%] px-2"> + <Text variant="caption/medium" color="steel-dark"> + Objects + </Text> + </div> + <div className="w-[30%]"> + <Text variant="caption/medium" color="steel-dark"> + Balance + </Text> + </div> + </div> + <div> + {displayedBalances + .slice((currentSlice - 1) * limit, currentSlice * limit) + .map((coin) => ( + <OwnedCoinView + id={id} + key={coin.coinType} + coin={coin} + /> + ))} + </div> + </div> {displayedBalances.length > limit && ( - <div className="flex flex-col justify-between gap-2 px-sm--rs py-xs--rs md:flex-row"> + <div className="flex flex-col justify-between gap-2 md:flex-row"> <Pagination hasFirst={currentSlice !== 1} onNext={() => setCurrentSlice(currentSlice + 1)} @@ -174,35 +196,36 @@ export function OwnedCoins({ id }: OwnerCoinsProps): JSX.Element { onFirst={() => setCurrentSlice(1)} /> <div className="flex items-center gap-3"> - <span className="text-body-sm text-neutral-40 dark:text-neutral-60"> + <Text variant="body/medium" color="steel-dark"> {`Showing `} {(currentSlice - 1) * limit + 1}- {currentSlice * limit > displayedBalances.length ? displayedBalances.length : currentSlice * limit} - </span> - <Select - dropdownPosition={DropdownPosition.Top} - value={limit.toString()} - options={[ - { label: '20 Per Page', id: '20' }, - { label: '40 Per Page', id: '40' }, - { label: '60 Per Page', id: '60' }, - ]} - onValueChange={(value) => { - setLimit(Number(value)); + </Text> + <select + className="form-select flex rounded-md border border-gray-45 px-3 py-2 pr-8 text-bodySmall font-medium leading-[1.2] text-steel-dark shadow-button" + value={limit} + onChange={(e) => { + setLimit(Number(e.target.value)); setCurrentSlice(1); }} - /> + > + <option value={20}>20 Per Page</option> + <option value={40}>40 Per Page</option> + <option value={60}>60 Per Page</option> + </select> </div> </div> )} </> - ) : ( + )} + + {!hasCoinsBalance && ( <div className="flex h-20 items-center justify-center md:h-coinsAndAssetsContainer"> - <span className="flex flex-row items-center gap-x-xs text-neutral-40 dark:text-neutral-60"> - No Coins Owned - </span> + <Text variant="body/medium" color="steel-dark"> + No Coins owned + </Text> </div> )} </div> @@ -210,67 +233,3 @@ export function OwnedCoins({ id }: OwnerCoinsProps): JSX.Element { </div> ); } - -interface FilterOption { - label: string; - isDisabled?: boolean; - counter?: number; - onClick: () => void; -} - -interface CoinsFilterProps { - filterOptions: FilterOption[]; -} - -function CoinsFilter({ filterOptions }: CoinsFilterProps) { - const [areFiltersVisible, setAreFiltersVisible] = useState<boolean>(false); - - function toggleFilterDropdown() { - setAreFiltersVisible(!areFiltersVisible); - } - - return ( - <div className="relative z-10"> - <Button type={ButtonType.Ghost} onClick={toggleFilterDropdown} icon={<FilterList />} /> - {areFiltersVisible && ( - <div className="absolute right-0"> - <Dropdown> - {filterOptions.map(({ onClick, counter, label, isDisabled }, index) => ( - <ListItem - isDisabled={isDisabled} - key={index} - onClick={() => { - onClick(); - toggleFilterDropdown(); - }} - hideBottomBorder - > - <div className="flex w-full flex-row gap-x-md"> - <span>{label}</span> - {counter && ( - <span className="ml-auto tabular-nums">{counter}</span> - )} - </div> - </ListItem> - ))} - </Dropdown> - </div> - )} - </div> - ); -} - -interface CoinListProps { - coins: CoinBalanceVerified[]; - id: string; -} - -function CoinList({ coins, id }: CoinListProps) { - return ( - <> - {coins.map((coin, index) => ( - <OwnedCoinView key={`${coin.coinType}-${index}`} coin={coin} id={id} /> - ))} - </> - ); -} diff --git a/apps/explorer/src/components/owned-coins/OwnedCoinsPanel.tsx b/apps/explorer/src/components/owned-coins/OwnedCoinsPanel.tsx index 062feaa633e..cb5c5c772f1 100644 --- a/apps/explorer/src/components/owned-coins/OwnedCoinsPanel.tsx +++ b/apps/explorer/src/components/owned-coins/OwnedCoinsPanel.tsx @@ -34,15 +34,15 @@ export default function CoinsPanel({ coinType, id }: CoinsPanelProps): JSX.Eleme const multiCols = containerWidth > MIN_CONTAINER_WIDTH_SIZE; return ( - <div className="max-h-[230px] overflow-auto"> - <div className="flex flex-col flex-wrap gap-xs" ref={coinsSectionRef}> + <div className="max-h-ownCoinsPanel overflow-auto pb-3"> + <div className="flex flex-wrap" ref={coinsSectionRef}> {data && data.pages.map((page) => page.data.map((coin) => ( <div key={coin.coinObjectId} className={clsx( - 'w-full', + 'w-full min-w-coinItemContainer pb-3 pl-3', multiCols && 'basis-1/3', !multiCols && 'pr-3', )} diff --git a/apps/explorer/src/components/owned-objects/ListView.tsx b/apps/explorer/src/components/owned-objects/ListView.tsx index 18af17be072..86032e74498 100644 --- a/apps/explorer/src/components/owned-objects/ListView.tsx +++ b/apps/explorer/src/components/owned-objects/ListView.tsx @@ -69,7 +69,7 @@ function ListViewItemContainer({ obj }: { obj: IotaObjectResponse }): JSX.Elemen subtitle={type} src={displayMeta?.image_url || ''} video={video} - variant="xxs" + variant="xs" /> <div className="flex flex-col overflow-hidden"> <OwnedObjectsText color="steel-darker" font="semibold"> diff --git a/apps/explorer/src/components/owned-objects/SmallThumbnailsView.tsx b/apps/explorer/src/components/owned-objects/SmallThumbnailsView.tsx index a75da63960c..9a3c97b3e2d 100644 --- a/apps/explorer/src/components/owned-objects/SmallThumbnailsView.tsx +++ b/apps/explorer/src/components/owned-objects/SmallThumbnailsView.tsx @@ -2,11 +2,12 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { Tooltip, TooltipPosition } from '@iota/apps-ui-kit'; import { type IotaObjectResponse } from '@iota/iota-sdk/client'; import { formatAddress } from '@iota/iota-sdk/utils'; -import { Info, Loader } from '@iota/ui-icons'; +import { Placeholder } from '@iota/ui'; import { type ReactNode } from 'react'; + +import { OwnedObjectsText } from '~/components'; import { ObjectLink, ObjectVideoImage } from '~/components/ui'; import { useResolveVideo } from '~/hooks/useResolveVideo'; import { parseObjectType, trimStdLibPrefix } from '~/lib/utils'; @@ -24,8 +25,8 @@ interface OwnObjectContainerProps { function OwnObjectContainer({ id, children }: OwnObjectContainerProps): JSX.Element { return ( - <div className="w-full min-w-[150px] basis-1/2 md:min-w-[210px] md:basis-1/3"> - <div className="rounded-xl p-xs hover:bg-neutral-92"> + <div className="w-full min-w-smallThumbNailsViewContainerMobile basis-1/2 pb-3 pr-4 md:min-w-smallThumbNailsViewContainer md:basis-1/4"> + <div className="rounded-lg p-2 hover:bg-hero/5"> <ObjectLink display="block" objectId={id} label={children} /> </div> </div> @@ -37,7 +38,7 @@ function SmallThumbnailsViewLoading({ limit }: { limit: number }): JSX.Element { <> {new Array(limit).fill(0).map((_, index) => ( <OwnObjectContainer key={index} id={String(index)}> - <Loader className="animate-spin" /> + <Placeholder rounded="lg" height="80px" /> </OwnObjectContainer> ))} </> @@ -53,7 +54,7 @@ function SmallThumbnail({ obj }: { obj: IotaObjectResponse }): JSX.Element { const id = obj.data?.objectId; return ( - <div className="flex items-center gap-md"> + <div className="group flex items-center gap-3.75 overflow-auto"> <ObjectVideoImage fadeIn disablePreview @@ -61,18 +62,16 @@ function SmallThumbnail({ obj }: { obj: IotaObjectResponse }): JSX.Element { subtitle={type} src={src} video={video} - variant="xs" + variant="small" /> - <div className="flex min-w-0 flex-col flex-nowrap gap-xxs"> - <span className="text-label-md text-neutral-10 dark:text-neutral-92">{name}</span> - <div className="flex flex-row items-center gap-xs text-label-md text-neutral-10 dark:text-neutral-92"> - <span className="text-label-sm text-neutral-40 dark:text-neutral-60"> - {formatAddress(id!)} - </span> - <Tooltip text={type} position={TooltipPosition.Bottom}> - <Info className="text-neutral-60 dark:text-neutral-40" /> - </Tooltip> - </div> + + <div className="flex min-w-0 flex-col flex-nowrap gap-1.25"> + <OwnedObjectsText color="steel-darker" font="semibold"> + {name} + </OwnedObjectsText> + <OwnedObjectsText color="steel-dark" font="medium"> + {formatAddress(id!)} + </OwnedObjectsText> </div> </div> ); @@ -84,7 +83,7 @@ export function SmallThumbnailsView({ limit, }: SmallThumbnailsViewProps): JSX.Element { return ( - <div className="flex h-full flex-row flex-wrap overflow-auto"> + <div className="flex flex-row flex-wrap overflow-auto"> {loading && <SmallThumbnailsViewLoading limit={limit} />} {data?.map((obj, index) => { const id = obj.data?.objectId; diff --git a/apps/explorer/src/components/owned-objects/ThumbnailsView.tsx b/apps/explorer/src/components/owned-objects/ThumbnailsView.tsx index 2ba1c84b54c..ef926db4fdb 100644 --- a/apps/explorer/src/components/owned-objects/ThumbnailsView.tsx +++ b/apps/explorer/src/components/owned-objects/ThumbnailsView.tsx @@ -4,7 +4,8 @@ import { type IotaObjectResponse } from '@iota/iota-sdk/client'; import { formatAddress } from '@iota/iota-sdk/utils'; -import { Loader } from '@iota/ui-icons'; +import { Placeholder, Text } from '@iota/ui'; + import { ObjectLink, ObjectVideoImage } from '~/components/ui'; import { useResolveVideo } from '~/hooks/useResolveVideo'; import { parseObjectType, trimStdLibPrefix } from '~/lib/utils'; @@ -34,8 +35,10 @@ function Thumbnail({ obj }: { obj: IotaObjectResponse }): JSX.Element { video={video} variant="medium" /> - <div className="absolute bottom-0 flex h-full w-full items-end justify-start rounded-xl p-xs opacity-0 transition-opacity duration-300 group-hover:bg-shader-neutral-light-48 group-hover:opacity-100 group-hover:transition group-hover:duration-300 group-hover:ease-in-out group-hover:dark:bg-shader-primary-dark-48"> - <span className="text-label-lg text-neutral-100">{displayName}</span> + <div className="absolute bottom-2 left-1/2 hidden w-10/12 -translate-x-1/2 justify-center rounded-lg bg-white/80 px-2 py-1 backdrop-blur group-hover:flex"> + <Text variant="subtitle/medium" color="steel-dark" truncate> + {displayName} + </Text> </div> </div> } @@ -48,8 +51,8 @@ function ThumbnailsOnlyLoading({ limit }: { limit: number }): JSX.Element { return ( <> {new Array(limit).fill(0).map((_, index) => ( - <div key={index} className="h-16 w-16 text-primary-30 md:h-31.5 md:w-31.5"> - <Loader className="animate-spin" /> + <div key={index} className="h-16 w-16 md:h-31.5 md:w-31.5"> + <Placeholder rounded="lg" height="100%" /> </div> ))} </> diff --git a/apps/explorer/src/components/top-validators-card/TopValidatorsCard.tsx b/apps/explorer/src/components/top-validators-card/TopValidatorsCard.tsx index ee08d4967db..dbc324cc8d6 100644 --- a/apps/explorer/src/components/top-validators-card/TopValidatorsCard.tsx +++ b/apps/explorer/src/components/top-validators-card/TopValidatorsCard.tsx @@ -4,28 +4,119 @@ import { useIotaClientQuery } from '@iota/dapp-kit'; import { ArrowRight12 } from '@iota/icons'; +import { type IotaValidatorSummary } from '@iota/iota-sdk/client'; import { Text } from '@iota/ui'; -import { useMemo } from 'react'; +import { type ReactNode, useMemo } from 'react'; -import { Banner, Link, PlaceholderTable, TableCard } from '~/components/ui'; -import { generateValidatorsTableData, type ValidatorTableColumn } from '~/lib/ui'; +import { HighlightedTableCol } from '~/components'; +import { + AddressLink, + Banner, + ImageIcon, + Link, + PlaceholderTable, + TableCard, + ValidatorLink, +} from '~/components/ui'; +import { ampli } from '~/lib/utils'; +import { StakeColumn } from './StakeColumn'; const NUMBER_OF_VALIDATORS = 10; -const VALIDATOR_COLUMNS: ValidatorTableColumn[] = [ - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'Address', - accessorKey: 'address', - }, - { - header: 'Stake', - accessorKey: 'stake', - }, -]; +export function processValidators(set: IotaValidatorSummary[]) { + return set.map((av) => ({ + name: av.name, + address: av.iotaAddress, + stake: av.stakingPoolIotaBalance, + logo: av.imageUrl, + })); +} +interface ValidatorData { + name: ReactNode; + stake: ReactNode; + delegation: ReactNode; + address: ReactNode; +} + +interface TableColumn { + header: string; + accessorKey: keyof ValidatorData; +} + +interface ValidatorsTableData { + data: ValidatorData[]; + columns: TableColumn[]; +} + +function validatorsTable( + validatorsData: IotaValidatorSummary[], + limit?: number, + showIcon?: boolean, +): ValidatorsTableData { + const validators = processValidators(validatorsData).sort(() => (Math.random() > 0.5 ? -1 : 1)); + + const validatorsItems = limit ? validators.splice(0, limit) : validators; + + return { + data: validatorsItems.map(({ name, stake, address, logo }) => ({ + name: ( + <HighlightedTableCol first> + <div className="flex items-center gap-2.5"> + {showIcon && ( + <ImageIcon src={logo} size="sm" fallback={name} label={name} circle /> + )} + <ValidatorLink + address={address} + label={name} + onClick={() => + ampli.clickedValidatorRow({ + sourceFlow: 'Top validators - validator name', + validatorAddress: address, + validatorName: name, + }) + } + /> + </div> + </HighlightedTableCol> + ), + stake: <StakeColumn stake={stake} />, + delegation: ( + <Text variant="bodySmall/medium" color="steel-darker"> + {stake.toString()} + </Text> + ), + address: ( + <HighlightedTableCol> + <AddressLink + address={address} + noTruncate={!limit} + onClick={() => + ampli.clickedValidatorRow({ + sourceFlow: 'Top validators - validator address', + validatorAddress: address, + validatorName: name, + }) + } + /> + </HighlightedTableCol> + ), + })), + columns: [ + { + header: 'Name', + accessorKey: 'name', + }, + { + header: 'Address', + accessorKey: 'address', + }, + { + header: 'Stake', + accessorKey: 'stake', + }, + ], + }; +} type TopValidatorsCardProps = { limit?: number; @@ -36,18 +127,7 @@ export function TopValidatorsCard({ limit, showIcon }: TopValidatorsCardProps): const { data, isPending, isSuccess, isError } = useIotaClientQuery('getLatestIotaSystemState'); const tableData = useMemo( - () => - data - ? generateValidatorsTableData({ - validators: [...data.activeValidators].sort(() => 0.5 - Math.random()), - atRiskValidators: [], - validatorEvents: [], - rollingAverageApys: null, - limit, - showValidatorIcon: showIcon, - columns: VALIDATOR_COLUMNS, - }) - : null, + () => (data ? validatorsTable(data.activeValidators, limit, showIcon) : null), [data, limit, showIcon], ); diff --git a/apps/explorer/src/components/ui/ImageIcon.tsx b/apps/explorer/src/components/ui/ImageIcon.tsx index 5b89cc52662..6ad06e132e1 100644 --- a/apps/explorer/src/components/ui/ImageIcon.tsx +++ b/apps/explorer/src/components/ui/ImageIcon.tsx @@ -9,7 +9,7 @@ const imageStyle = cva(['text-white capitalize overflow-hidden bg-gray-40'], { variants: { size: { sm: 'w-6 h-6 font-medium text-subtitleSmallExtra', - md: 'w-8 h-8 text-label-lg', + md: 'w-7.5 h-7.5 font-medium text-body', lg: 'md:w-10 md:h-10 w-8 h-8 font-medium text-heading4 md:text-iconTextLarge', xl: 'md:w-31.5 md:h-31.5 w-16 h-16 font-medium text-heading4 md:text-iconTextLarge', }, @@ -38,7 +38,7 @@ interface FallBackAvatarProps { function FallBackAvatar({ fallback }: FallBackAvatarProps): JSX.Element { return ( - <div className="flex h-full w-full items-center justify-center bg-neutral-90 text-neutral-10"> + <div className="flex h-full w-full items-center justify-center bg-neutral-80"> {fallback?.slice(0, 2)} </div> ); diff --git a/apps/explorer/src/components/ui/InternalLink.tsx b/apps/explorer/src/components/ui/InternalLink.tsx index 35569fba80e..13bc566cd87 100644 --- a/apps/explorer/src/components/ui/InternalLink.tsx +++ b/apps/explorer/src/components/ui/InternalLink.tsx @@ -2,6 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { isIotaNSName } from '@iota/core'; import { formatAddress, formatDigest } from '@iota/iota-sdk/utils'; import { type ReactNode } from 'react'; @@ -40,9 +41,12 @@ function createInternalLink<T extends string>( export const EpochLink = createInternalLink('epoch', 'epoch'); export const CheckpointLink = createInternalLink('checkpoint', 'digest', formatAddress); export const CheckpointSequenceLink = createInternalLink('checkpoint', 'sequence'); -export const AddressLink = createInternalLink('address', 'address', (addressOrNs) => - formatAddress(addressOrNs), -); +export const AddressLink = createInternalLink('address', 'address', (addressOrNs) => { + if (isIotaNSName(addressOrNs)) { + return addressOrNs; + } + return formatAddress(addressOrNs); +}); export const ObjectLink = createInternalLink('object', 'objectId', formatAddress); export const TransactionLink = createInternalLink('txblock', 'digest', formatDigest); export const ValidatorLink = createInternalLink('validator', 'address', formatAddress); diff --git a/apps/explorer/src/components/ui/ObjectVideoImage.tsx b/apps/explorer/src/components/ui/ObjectVideoImage.tsx index 15dddedfebe..08253b387f9 100644 --- a/apps/explorer/src/components/ui/ObjectVideoImage.tsx +++ b/apps/explorer/src/components/ui/ObjectVideoImage.tsx @@ -11,8 +11,7 @@ import { Image, ObjectModal, type ImageProps } from '~/components/ui'; const imageStyles = cva(['z-0 flex-shrink-0 relative'], { variants: { variant: { - xxs: 'h-8 w-8', - xs: 'h-12 w-12', + xs: 'h-8 w-8', small: 'h-16 w-16', medium: 'md:h-31.5 md:w-31.5 h-16 w-16', large: 'h-50 w-50', diff --git a/apps/explorer/src/components/ui/Pagination.tsx b/apps/explorer/src/components/ui/Pagination.tsx index af200b605f0..b22dd8226ee 100644 --- a/apps/explorer/src/components/ui/Pagination.tsx +++ b/apps/explorer/src/components/ui/Pagination.tsx @@ -2,8 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { Button, ButtonSize, ButtonType } from '@iota/apps-ui-kit'; -import { ArrowLeft, ArrowRight, DoubleArrowLeft } from '@iota/ui-icons'; +import { PaginationFirst24, PaginationNext24, PaginationPrev24 } from '@iota/icons'; import { type InfiniteData, type UseInfiniteQueryResult } from '@tanstack/react-query'; import { useState } from 'react'; @@ -88,6 +87,32 @@ export function usePaginationStack<Cursor = string>() { }; } +interface PaginationButtonProps { + label: string; + icon: typeof PaginationFirst24; + disabled: boolean; + onClick(): void; +} + +function PaginationButton({ + label, + icon: Icon, + disabled, + onClick, +}: PaginationButtonProps): JSX.Element { + return ( + <button + className="rounded-md border border-steel px-2 py-1 text-steel shadow-xs disabled:border-gray-45 disabled:text-gray-45" + aria-label={label} + type="button" + disabled={disabled} + onClick={onClick} + > + <Icon className="text-[24px]" /> + </button> + ); +} + export function Pagination({ hasNext, hasPrev, @@ -97,24 +122,21 @@ export function Pagination({ }: PaginationProps): JSX.Element { return ( <div className="flex gap-2"> - <Button - type={ButtonType.Secondary} - size={ButtonSize.Small} - icon={<DoubleArrowLeft />} - onClick={onFirst} + <PaginationButton + label="Go to First" + icon={PaginationFirst24} disabled={!hasPrev} + onClick={onFirst} /> - <Button - type={ButtonType.Secondary} - size={ButtonSize.Small} - icon={<ArrowLeft />} - onClick={onPrev} + <PaginationButton + label="Previous" + icon={PaginationPrev24} disabled={!hasPrev} + onClick={onPrev} /> - <Button - type={ButtonType.Secondary} - size={ButtonSize.Small} - icon={<ArrowRight />} + <PaginationButton + label="Next" + icon={PaginationNext24} disabled={!hasNext} onClick={onNext} /> diff --git a/apps/explorer/src/components/ui/RingChart.tsx b/apps/explorer/src/components/ui/RingChart.tsx index 7f320e307b6..0b3e723d545 100644 --- a/apps/explorer/src/components/ui/RingChart.tsx +++ b/apps/explorer/src/components/ui/RingChart.tsx @@ -2,6 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { Heading } from '@iota/ui'; import clsx from 'clsx'; import { Fragment } from 'react'; @@ -19,6 +20,7 @@ type RingChartData = { interface RingChartLegendProps { data: RingChartData; + title: string; } function getColorFromGradient({ deg, values }: Gradient): string { @@ -35,48 +37,51 @@ function getColorFromGradient({ deg, values }: Gradient): string { return `linear-gradient(${gradientResult.join(',')})`; } -export function RingChartLegend({ data }: RingChartLegendProps): JSX.Element { +export function RingChartLegend({ data, title }: RingChartLegendProps): JSX.Element { return ( - <> - {data.map(({ color, gradient, label, value }) => { - const colorDisplay = gradient ? getColorFromGradient(gradient) : color; - - return ( - <div - className={clsx('flex items-center gap-xxs', value === 0 && 'hidden')} - key={label} - > + <div className="flex flex-col gap-2"> + <Heading variant="heading5/semibold" color="steel-darker"> + {title} + </Heading> + + <div className="flex flex-col items-start justify-center gap-2"> + {data.map(({ color, gradient, label, value }) => { + const colorDisplay = gradient ? getColorFromGradient(gradient) : color; + + return ( <div - style={{ background: colorDisplay }} - className="h-1.5 w-1.5 rounded-full" - /> - <div className="text-label-md text-neutral-10"> - {value} {label} + className={clsx('flex items-center gap-1.5', value === 0 && 'hidden')} + key={label} + > + <div + style={{ background: colorDisplay }} + className="h-3 w-3 rounded-sm" + /> + <div + style={{ + backgroundImage: colorDisplay, + color: colorDisplay, + }} + className="bg-clip-text text-body font-medium text-transparent" + > + {value} {label} + </div> </div> - </div> - ); - })} - </> + ); + })} + </div> + </div> ); } -interface RingChartProperties { - cx?: number; - cy?: number; - radius?: number; - width?: number; -} - -export interface RingChartProps extends RingChartProperties { +export interface RingChartProps { data: RingChartData; } -export function RingChart({ - data, - cx = 25, - cy = 25, - radius = 20, - width = 5, -}: RingChartProps): JSX.Element { + +export function RingChart({ data }: RingChartProps): JSX.Element { + const radius = 20; + const cx = 25; + const cy = 25; const dashArray = 2 * Math.PI * radius; const startAngle = -90; const total = data.reduce((acc, { value }) => acc + value, 0); @@ -86,9 +91,8 @@ export function RingChart({ const gradientId = `gradient-${idx}`; const ratio = (100 / total) * value; const angle = (filled * 360) / 100 + startAngle; - const offset = dashArray - (dashArray * (ratio * 0.98)) / 100; + const offset = dashArray - (dashArray * ratio) / 100; filled += ratio; - return ( <Fragment key={label}> {gradient && ( @@ -106,10 +110,9 @@ export function RingChart({ r={radius} fill="transparent" stroke={gradient ? `url(#${gradientId})` : color} - strokeWidth={width} + strokeWidth={5} strokeDasharray={dashArray} strokeDashoffset={offset} - strokeLinecap="round" transform={`rotate(${angle} ${cx} ${cy})`} /> </Fragment> @@ -118,12 +121,14 @@ export function RingChart({ return ( <div className="relative"> - <svg viewBox="0 0 50 50" strokeLinecap="round"> + <svg viewBox="0 0 50 50" strokeLinecap="butt"> {segments} </svg> <div className="absolute inset-0 mx-auto flex items-center justify-center"> <div className="flex flex-col items-center gap-1.5"> - <span className="text-title-md text-neutral-10">{total}</span> + <Heading variant="heading2/semibold" color="iota-dark"> + {total} + </Heading> </div> </div> </div> diff --git a/apps/explorer/src/components/ui/TableCard.tsx b/apps/explorer/src/components/ui/TableCard.tsx index 98572fbcd6f..5768cc5f779 100644 --- a/apps/explorer/src/components/ui/TableCard.tsx +++ b/apps/explorer/src/components/ui/TableCard.tsx @@ -17,21 +17,19 @@ import { type ColumnDef, getCoreRowModel, getSortedRowModel, - type RowData, type SortingState, useReactTable, } from '@tanstack/react-table'; import clsx from 'clsx'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useNavigateWithQuery } from './LinkWithQuery'; -export interface TableCardProps<DataType extends RowData> { +export interface TableCardProps<DataType extends object> { refetching?: boolean; data: DataType[]; columns: ColumnDef<DataType>[]; sortTable?: boolean; defaultSorting?: SortingState; - areHeadersCentered?: boolean; paginationOptions?: TablePaginationOptions; totalLabel?: string; viewAll?: string; @@ -43,7 +41,6 @@ export function TableCard<DataType extends object>({ columns, sortTable, defaultSorting, - areHeadersCentered, paginationOptions, totalLabel, viewAll, @@ -51,9 +48,22 @@ export function TableCard<DataType extends object>({ const navigate = useNavigateWithQuery(); const [sorting, setSorting] = useState<SortingState>(defaultSorting || []); + // Use Columns to create a table + const processedcol = useMemo<ColumnDef<DataType>[]>( + () => + columns.map((column) => ({ + ...column, + // cell renderer for each column from react-table + // cell should be in the column definition + //TODO: move cell to column definition + ...(!sortTable && { cell: ({ getValue }) => getValue() }), + })), + [columns, sortTable], + ); + const table = useReactTable({ data, - columns, + columns: processedcol, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting, @@ -96,7 +106,6 @@ export function TableCard<DataType extends object>({ ? column.getToggleSortingHandler() : undefined } - isContentCentered={areHeadersCentered} /> ))} </TableHeaderRow> diff --git a/apps/explorer/src/components/ui/image/Image.tsx b/apps/explorer/src/components/ui/image/Image.tsx index 17898945c9a..b61c47f9b91 100644 --- a/apps/explorer/src/components/ui/image/Image.tsx +++ b/apps/explorer/src/components/ui/image/Image.tsx @@ -2,8 +2,8 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { EyeClose16, NftTypeImage24 } from '@iota/icons'; import { LoadingIndicator } from '@iota/ui'; -import { PlaceholderReplace, VisibilityOff } from '@iota/ui-icons'; import { cva, cx, type VariantProps } from 'class-variance-authority'; import clsx from 'clsx'; import { useAnimate } from 'framer-motion'; @@ -92,7 +92,7 @@ function BaseImage({ ref={scope} className={cx( imageStyles({ size, rounded, aspect }), - 'relative flex items-center justify-center bg-neutral-96 text-neutral-40 dark:bg-neutral-10 dark:text-neutral-60', + 'relative flex items-center justify-center bg-gray-40 text-gray-65', animateFadeIn && 'opacity-0', )} > @@ -102,17 +102,17 @@ function BaseImage({ isBlurred && ( <div className={clsx( - 'absolute z-20 flex h-full w-full items-center justify-center rounded-md bg-neutral-10/30 text-center text-white backdrop-blur-md', + 'absolute z-20 flex h-full w-full items-center justify-center rounded-md bg-gray-100/30 text-center text-white backdrop-blur-md', visibility === ImageVisibility.Hide && 'pointer-events-none cursor-not-allowed', )} onClick={() => setIsBlurred(!isBlurred)} > - <VisibilityOff /> + <EyeClose16 /> </div> ) ) : status === 'failed' ? ( - <PlaceholderReplace /> + <NftTypeImage24 /> ) : null} {status === 'loaded' && ( <img diff --git a/apps/explorer/src/components/validator/ValidatorStats.tsx b/apps/explorer/src/components/validator/ValidatorStats.tsx index b4bfbe5088c..940cb4e85c6 100644 --- a/apps/explorer/src/components/validator/ValidatorStats.tsx +++ b/apps/explorer/src/components/validator/ValidatorStats.tsx @@ -52,6 +52,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Staking APY" text={apy === null ? 'N/A' : `${apy}%`} + showSupportingLabel={false} tooltipText="This represents the Annualized Percentage Yield based on a specific validator's past activities. Keep in mind that this APY may not hold true in the future." tooltipPosition={TooltipPosition.Right} /> @@ -60,6 +61,7 @@ export function ValidatorStats({ label="Total IOTA Staked" text={formattedTotalStakeAmount} supportingLabel={totalStakeSymbol} + showSupportingLabel tooltipText="The total amount of IOTA staked on the network by validators and delegators to secure the network and earn rewards." tooltipPosition={TooltipPosition.Right} /> @@ -69,6 +71,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Commission" text={`${commission}%`} + showSupportingLabel={false} tooltipText="The charge imposed by the validator for their staking services." tooltipPosition={TooltipPosition.Right} /> @@ -76,6 +79,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Delegators" text={numberOfDelegators || '--'} + showSupportingLabel={false} tooltipText={ !numberOfDelegators ? 'Coming soon' @@ -93,6 +97,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Last Epoch Rewards" text={typeof epochRewards === 'number' ? formattedEpochRewards : '0'} + showSupportingLabel supportingLabel={epochRewardsSymbol} tooltipText={ epochRewards === null @@ -106,6 +111,7 @@ export function ValidatorStats({ label="Reward Pool" text={formattedRewardsPoolBalance} supportingLabel={rewardsPoolBalanceSymbol} + showSupportingLabel tooltipText={ Number(rewardsPoolBalance) <= 0 ? 'Coming soon' @@ -123,6 +129,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Checkpoint Participation" text={networkStakingParticipation || '--'} + showSupportingLabel={false} tooltipText={ !networkStakingParticipation ? 'Coming soon' @@ -134,6 +141,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Voted Last Round" text={votedLastRound || '--'} + showSupportingLabel={false} tooltipText={ !votedLastRound ? 'Coming soon' @@ -147,6 +155,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Tallying Score" text={tallyingScore ?? '--'} + showSupportingLabel={false} tooltipText={ !tallyingScore ? 'Coming soon' @@ -158,6 +167,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Last Narwhal round" text={lastNarwhalRound || '--'} + showSupportingLabel={false} tooltipText={ !lastNarwhalRound ? 'Coming soon' @@ -171,6 +181,7 @@ export function ValidatorStats({ size={LabelTextSize.Medium} label="Proposed next epoch gas price" text={nextEpochGasPriceAmount} + showSupportingLabel supportingLabel="nano" tooltipText="The gas price estimate provided by this validator for the upcoming epoch." tooltipPosition={TooltipPosition.Right} diff --git a/apps/explorer/src/hooks/useSearch.ts b/apps/explorer/src/hooks/useSearch.ts index c73650e7e2b..b77683cc876 100644 --- a/apps/explorer/src/hooks/useSearch.ts +++ b/apps/explorer/src/hooks/useSearch.ts @@ -2,6 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { isIotaNSName, useIotaNSEnabled } from '@iota/core'; import { useIotaClientQuery, useIotaClient } from '@iota/dapp-kit'; import { type IotaClient, type IotaSystemStateSummary } from '@iota/iota-sdk/client'; import { @@ -66,7 +67,23 @@ const getResultsForCheckpoint = async ( ]; }; -const getResultsForAddress = async (client: IotaClient, query: string): Promise<Results | null> => { +const getResultsForAddress = async ( + client: IotaClient, + query: string, + iotaNSEnabled: boolean, +): Promise<Results | null> => { + if (iotaNSEnabled && isIotaNSName(query)) { + const resolved = await client.resolveNameServiceAddress({ name: query.toLowerCase() }); + if (!resolved) return null; + return [ + { + id: resolved, + label: resolved, + type: 'address', + }, + ]; + } + const normalized = normalizeIotaObjectId(query); if (!isValidIotaAddress(normalized) || isGenesisLibAddress(normalized)) return null; @@ -131,6 +148,7 @@ const getResultsForValidatorByPoolIdOrIotaAddress = async ( export function useSearch(query: string): UseQueryResult<Results, Error> { const client = useIotaClient(); const { data: systemStateSummery } = useIotaClientQuery('getLatestIotaSystemState'); + const iotaNSEnabled = useIotaNSEnabled(); return useQuery<Results, Error>({ // eslint-disable-next-line @tanstack/query/exhaustive-deps @@ -140,7 +158,7 @@ export function useSearch(query: string): UseQueryResult<Results, Error> { await Promise.allSettled([ getResultsForTransaction(client, query), getResultsForCheckpoint(client, query), - getResultsForAddress(client, query), + getResultsForAddress(client, query, iotaNSEnabled), getResultsForObject(client, query), getResultsForValidatorByPoolIdOrIotaAddress(systemStateSummery || null, query), ]) diff --git a/apps/explorer/src/lib/ui/utils/generateValidatorsTableData.tsx b/apps/explorer/src/lib/ui/utils/generateValidatorsTableData.tsx deleted file mode 100644 index 3eabb06a9af..00000000000 --- a/apps/explorer/src/lib/ui/utils/generateValidatorsTableData.tsx +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; -import { - BadgeType, - type TableCellProps, - TableCellType, - TableCellTextColor, -} from '@iota/apps-ui-kit'; -import type { ColumnDef } from '@tanstack/react-table'; -import { type ApyByValidator, formatPercentageDisplay } from '@iota/core'; - -import { ampli, getValidatorMoveEvent, VALIDATOR_LOW_STAKE_GRACE_PERIOD } from '~/lib'; -import { Link, StakeColumn, AddressLink, ImageIcon } from '~/components'; -import type { IotaEvent, IotaValidatorSummary } from '@iota/iota-sdk/dist/cjs/client'; - -interface ValidatorTableRow { - name: TableCellProps; - stake: TableCellProps; - apy: TableCellProps; - nextEpochGasPrice: TableCellProps; - commission: TableCellProps; - address: TableCellProps; - lastReward: TableCellProps; - votingPower: TableCellProps; - atRisk: TableCellProps; -} - -interface GenerateValidatorsTableDataArgs { - validators: IotaValidatorSummary[]; - atRiskValidators: [string, string][]; - validatorEvents: IotaEvent[]; - rollingAverageApys: ApyByValidator | null; - limit?: number; - showValidatorIcon?: boolean; - columns?: ColumnDef<object, unknown>[]; -} - -const ALL_VALIDATOR_COLUMNS = [ - { - header: '#', - accessorKey: 'number', - }, - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'Stake', - accessorKey: 'stake', - }, - { - header: 'Proposed Next Epoch Gas Price', - accessorKey: 'nextEpochGasPrice', - }, - { - header: 'Address', - accessorKey: 'address', - }, - { - header: 'APY', - accessorKey: 'apy', - }, - { - header: 'Commission', - accessorKey: 'commission', - }, - { - header: 'Last Epoch Rewards', - accessorKey: 'lastReward', - }, - { - header: 'Voting Power', - accessorKey: 'votingPower', - }, - { - header: 'Status', - accessorKey: 'atRisk', - }, -] as const; - -type AccessorKey = (typeof ALL_VALIDATOR_COLUMNS)[number]['accessorKey']; - -export interface ValidatorTableColumn { - header: string; - accessorKey: AccessorKey; -} - -const DEFAULT_COLUMNS: ValidatorTableColumn[] = [ - { - header: '#', - accessorKey: 'number', - }, - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'Stake', - accessorKey: 'stake', - }, - { - header: 'Proposed Next Epoch Gas Price', - accessorKey: 'nextEpochGasPrice', - }, - { - header: 'APY', - accessorKey: 'apy', - }, - { - header: 'Commission', - accessorKey: 'commission', - }, - { - header: 'Last Epoch Rewards', - accessorKey: 'lastReward', - }, - { - header: 'Voting Power', - accessorKey: 'votingPower', - }, - { - header: 'Status', - accessorKey: 'atRisk', - }, -]; - -function generateValidatorAtRisk(atRisk: number | null): TableCellProps { - if (atRisk === null) { - return { - type: TableCellType.Badge, - badgeType: BadgeType.PrimarySoft, - label: 'Active', - }; - } - - const atRiskText = atRisk > 1 ? `in ${atRisk} epochs` : 'next epoch'; - return { - type: TableCellType.Badge, - badgeType: BadgeType.Neutral, - label: `At Risk ${atRiskText}`, - }; -} - -function ValidatorName({ - address, - name, - imageUrl, -}: { - address: string; - name: string; - imageUrl: string; -}) { - return ( - <Link - to={`/validator/${encodeURIComponent(address)}`} - onClick={() => - ampli.clickedValidatorRow({ - sourceFlow: 'Epoch details', - validatorAddress: address, - validatorName: name, - }) - } - > - <div className="flex items-center gap-x-2.5 text-neutral-40 dark:text-neutral-60"> - <ImageIcon src={imageUrl} size="sm" label={name} fallback={name} /> - <span className="text-label-lg">{name}</span> - </div> - </Link> - ); -} - -function ValidatorAddress({ - address, - name, - limit, -}: { - address: string; - name: string; - limit?: number; -}) { - return ( - <div className="whitespace-nowrap"> - <AddressLink - address={address} - noTruncate={!limit} - onClick={() => - ampli.clickedValidatorRow({ - sourceFlow: 'Top validators - validator address', - validatorAddress: address, - validatorName: name, - }) - } - /> - </div> - ); -} - -export function generateValidatorsTableData({ - validators, - limit, - atRiskValidators = [], - validatorEvents = [], - rollingAverageApys = null, - showValidatorIcon = true, - columns = DEFAULT_COLUMNS, -}: GenerateValidatorsTableDataArgs): { - data: ValidatorTableRow[]; - columns: ColumnDef<object, unknown>[]; -} { - return { - data: validators.map((validator, i) => { - const validatorName = validator.name; - const totalStake = validator.stakingPoolIotaBalance; - - const event = getValidatorMoveEvent(validatorEvents, validator.iotaAddress) as { - pool_staking_reward?: string; - }; - - const atRiskValidator = atRiskValidators.find( - ([address]) => address === validator.iotaAddress, - ); - const isAtRisk = !!atRiskValidator; - const lastReward = event?.pool_staking_reward ?? null; - const { apy, isApyApproxZero } = rollingAverageApys?.[validator.iotaAddress] ?? { - apy: null, - }; - - return { - number: { - type: TableCellType.Text, - label: `${i + 1}`, - textColor: TableCellTextColor.Dark, - }, - name: showValidatorIcon - ? { - type: TableCellType.Children, - children: ( - <ValidatorName - address={validator.iotaAddress} - name={validatorName} - imageUrl={validator.imageUrl} - /> - ), - } - : { - type: TableCellType.Text, - label: validatorName, - textColor: TableCellTextColor.Dark, - }, - stake: { - type: TableCellType.Children, - children: <StakeColumn stake={totalStake} />, - noWrap: true, - }, - nextEpochGasPrice: { - type: TableCellType.Children, - children: <StakeColumn stake={validator.nextEpochGasPrice} inNano />, - noWrap: true, - }, - apy: { - type: TableCellType.Text, - label: formatPercentageDisplay(apy, '--', isApyApproxZero), - }, - commission: { - type: TableCellType.Text, - label: `${Number(validator.commissionRate) / 100}%`, - }, - lastReward: - lastReward !== null - ? { - type: TableCellType.Children, - children: <StakeColumn stake={Number(lastReward)} />, - noWrap: true, - } - : { - type: TableCellType.Text, - label: '--', - }, - votingPower: { - type: TableCellType.Text, - label: validator.votingPower ? Number(validator.votingPower) / 100 + '%' : '--', - }, - atRisk: generateValidatorAtRisk( - isAtRisk ? VALIDATOR_LOW_STAKE_GRACE_PERIOD - Number(atRiskValidator[1]) : null, - ), - address: { - type: TableCellType.Children, - children: ( - <ValidatorAddress - address={validator.iotaAddress} - name={validatorName} - limit={limit} - /> - ), - }, - }; - }), - columns: columns, - }; -} diff --git a/apps/explorer/src/lib/ui/utils/index.ts b/apps/explorer/src/lib/ui/utils/index.ts index 217017f0324..9c93389b7d4 100644 --- a/apps/explorer/src/lib/ui/utils/index.ts +++ b/apps/explorer/src/lib/ui/utils/index.ts @@ -3,5 +3,4 @@ export * from './generateTableDataFromCheckpointsData'; export * from './generateTableDataFromEpochsData'; -export * from './generateValidatorsTableData'; export * from './objectField'; diff --git a/apps/explorer/src/pages/address-result/AddressResult.tsx b/apps/explorer/src/pages/address-result/AddressResult.tsx index 44653c77bba..09401b1b446 100644 --- a/apps/explorer/src/pages/address-result/AddressResult.tsx +++ b/apps/explorer/src/pages/address-result/AddressResult.tsx @@ -2,6 +2,9 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { isIotaNSName, useResolveIotaNSAddress, useResolveIotaNSName } from '@iota/core'; +import { Domain32 } from '@iota/icons'; +import { LoadingIndicator } from '@iota/ui'; import { useParams } from 'react-router-dom'; import { @@ -11,61 +14,47 @@ import { PageLayout, TransactionsForAddress, } from '~/components'; -import { Divider, PageHeader, SplitPanes } from '~/components/ui'; - +import { Divider, PageHeader, SplitPanes, TabHeader, TabsList, TabsTrigger } from '~/components/ui'; import { useBreakpoint } from '~/hooks/useBreakpoint'; import { LocalStorageSplitPaneKey } from '~/lib/enums'; -import { Panel, Title, Divider as DividerUI } from '@iota/apps-ui-kit'; import { TotalStaked } from './TotalStaked'; -const LEFT_RIGHT_PANEL_MIN_SIZE = 30; - interface AddressResultPageHeaderProps { address: string; + loading?: boolean; } -function AddressResultPageHeader({ address }: AddressResultPageHeaderProps): JSX.Element { - return <PageHeader type="Address" title={address} after={<TotalStaked address={address} />} />; +const LEFT_RIGHT_PANEL_MIN_SIZE = 30; +const TOP_PANEL_MIN_SIZE = 20; + +interface AddressResultPageHeaderProps { + address: string; + loading?: boolean; } +function AddressResultPageHeader({ address, loading }: AddressResultPageHeaderProps): JSX.Element { + const { data: domainName, isLoading } = useResolveIotaNSName(address); -function AddressResult({ address }: { address: string }): JSX.Element { return ( - <> - <Panel> - <Title title="Owned Objects" /> - <DividerUI /> - <div className="flex flex-col gap-2xl"> - <OwnedObjectsPanel address={address} /> - </div> - </Panel> - - <Panel> - <Title title="Transaction Blocks" /> - <div className="flex flex-col gap-2xl p-md--rs"> - <TransactionBlocksPanel address={address} /> - </div> - </Panel> - </> + <PageHeader + loading={loading || isLoading} + type="Address" + title={address} + subtitle={domainName} + before={<Domain32 className="h-6 w-6 text-steel-darker sm:h-10 sm:w-10" />} + after={<TotalStaked address={address} />} + /> ); } -export default function AddressResultPage(): JSX.Element { - const { id } = useParams(); +function IotaNSAddressResultPageHeader({ name }: { name: string }): JSX.Element { + const { data: address, isLoading } = useResolveIotaNSAddress(name); - return ( - <PageLayout - content={ - <div className="flex flex-col gap-2xl"> - <AddressResultPageHeader address={id!} /> - <AddressResult address={id!} /> - </div> - } - /> - ); + return <AddressResultPageHeader address={address ?? name} loading={isLoading} />; } -function OwnedObjectsPanel({ address }: { address: string }) { +function AddressResult({ address }: { address: string }): JSX.Element { const isMediumOrAbove = useBreakpoint('md'); + const leftPane = { panel: <OwnedCoins id={address} />, minSize: LEFT_RIGHT_PANEL_MIN_SIZE, @@ -77,36 +66,106 @@ function OwnedObjectsPanel({ address }: { address: string }) { minSize: LEFT_RIGHT_PANEL_MIN_SIZE, }; + const topPane = { + panel: ( + <div className="flex h-full flex-col justify-between"> + <ErrorBoundary> + {isMediumOrAbove ? ( + <SplitPanes + autoSaveId={LocalStorageSplitPaneKey.AddressViewHorizontal} + dividerSize="none" + splitPanels={[leftPane, rightPane]} + direction="horizontal" + /> + ) : ( + <> + {leftPane.panel} + <div className="my-8"> + <Divider /> + </div> + {rightPane.panel} + </> + )} + </ErrorBoundary> + </div> + ), + minSize: TOP_PANEL_MIN_SIZE, + }; + + const bottomPane = { + panel: ( + <div className="flex h-full flex-col pt-12"> + <TabsList> + <TabsTrigger value="tab">Transaction Blocks</TabsTrigger> + </TabsList> + + <ErrorBoundary> + <div data-testid="tx" className="relative mt-4 h-full min-h-14 overflow-auto"> + <TransactionsForAddress address={address} type="address" /> + </div> + </ErrorBoundary> + + <div className="mt-0.5"> + <Divider /> + </div> + </div> + ), + }; + return ( - <div className="flex h-[800px] flex-col justify-between"> - <ErrorBoundary> - {isMediumOrAbove ? ( + <TabHeader title="Owned Objects" noGap> + {isMediumOrAbove ? ( + <div className="h-300"> <SplitPanes - autoSaveId={LocalStorageSplitPaneKey.AddressViewHorizontal} + autoSaveId={LocalStorageSplitPaneKey.AddressViewVertical} dividerSize="none" - splitPanels={[leftPane, rightPane]} - direction="horizontal" + splitPanels={[topPane, bottomPane]} + direction="vertical" /> - ) : ( - <> - {leftPane.panel} - <div className="my-8"> - <Divider /> - </div> - {rightPane.panel} - </> - )} - </ErrorBoundary> - </div> + </div> + ) : ( + <> + {topPane.panel} + <div className="mt-5"> + <Divider /> + </div> + {bottomPane.panel} + </> + )} + </TabHeader> ); } -function TransactionBlocksPanel({ address }: { address: string }) { +function IotaNSAddressResult({ name }: { name: string }): JSX.Element { + const { isFetched, data } = useResolveIotaNSAddress(name); + + if (!isFetched) { + return <LoadingIndicator />; + } + + // Fall back into just trying to load the name as an address anyway: + return <AddressResult address={data ?? name} />; +} + +export default function AddressResultPage(): JSX.Element { + const { id } = useParams(); + const isIotaNSAddress = isIotaNSName(id!); + return ( - <ErrorBoundary> - <div data-testid="tx" className="relative mt-4 h-full min-h-14 overflow-auto"> - <TransactionsForAddress address={address} type="address" /> - </div> - </ErrorBoundary> + <PageLayout + content={ + isIotaNSAddress ? ( + <> + <IotaNSAddressResultPageHeader name={id!} /> + <IotaNSAddressResult name={id!} /> + </> + ) : ( + <> + <AddressResultPageHeader address={id!} /> + <AddressResult address={id!} /> + </> + ) + } + /> ); } diff --git a/apps/explorer/src/pages/checkpoints/CheckpointDetail.tsx b/apps/explorer/src/pages/checkpoints/CheckpointDetail.tsx index e945afe33d1..5d57161657f 100644 --- a/apps/explorer/src/pages/checkpoints/CheckpointDetail.tsx +++ b/apps/explorer/src/pages/checkpoints/CheckpointDetail.tsx @@ -99,11 +99,13 @@ export default function CheckpointDetail(): JSX.Element { size={LabelTextSize.Medium} label="Checkpoint Sequence No." text={data.sequenceNumber} + showSupportingLabel={false} /> <LabelText size={LabelTextSize.Medium} label="Epoch" text={data.epoch} + showSupportingLabel={false} /> <LabelText size={LabelTextSize.Medium} @@ -125,6 +127,7 @@ export default function CheckpointDetail(): JSX.Element { }) : '--' } + showSupportingLabel={false} /> </div> ) : null} @@ -153,6 +156,7 @@ export default function CheckpointDetail(): JSX.Element { size={LabelTextSize.Medium} label="Aggregated Validator Signature" text={data.validatorSignature} + showSupportingLabel={false} /> </div> ) : null} @@ -179,18 +183,21 @@ export default function CheckpointDetail(): JSX.Element { size={LabelTextSize.Medium} label="Computation Fee" text={formattedComputationCost} + showSupportingLabel supportingLabel={computationCostCoinType} /> <LabelText size={LabelTextSize.Medium} label="Storage Fee" text={formattedStorageCost} + showSupportingLabel supportingLabel={storageCostCoinType} /> <LabelText size={LabelTextSize.Medium} label="Storage Rebate" text={formattedStorageRebate} + showSupportingLabel supportingLabel={storageRebateCoinType} /> </div> diff --git a/apps/explorer/src/pages/epochs/EpochDetail.tsx b/apps/explorer/src/pages/epochs/EpochDetail.tsx index 43a0c3159e8..d5d52c81ad9 100644 --- a/apps/explorer/src/pages/epochs/EpochDetail.tsx +++ b/apps/explorer/src/pages/epochs/EpochDetail.tsx @@ -2,7 +2,9 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { CoinFormat, useFormatCoin } from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { LoadingIndicator } from '@iota/ui'; import { useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; @@ -15,53 +17,35 @@ import { } from '@iota/apps-ui-kit'; import { CheckpointsTable, PageLayout } from '~/components'; -import { Banner, TableCard } from '~/components/ui'; +import { Banner, Stats, type StatsProps, TableCard } from '~/components/ui'; import { useEnhancedRpcClient } from '~/hooks/useEnhancedRpc'; -import { EpochStats, EpochStatsGrid } from './stats/EpochStats'; +import { getEpochStorageFundFlow, getSupplyChangeAfterEpochEnd } from '~/lib/utils'; +import { validatorsTableData } from '../validators/Validators'; +import { EpochProgress } from './stats/EpochProgress'; +import { EpochStats } from './stats/EpochStats'; import { ValidatorStatus } from './stats/ValidatorStatus'; -import cx from 'clsx'; -import { TokenStats } from './stats/TokenStats'; -import { EpochTopStats } from './stats/EpochTopStats'; -import { getEpochStorageFundFlow } from '~/lib/utils'; -import { - generateValidatorsTableData, - type ValidatorTableColumn, -} from '~/lib/ui/utils/generateValidatorsTableData'; - -export const VALIDATOR_COLUMNS: ValidatorTableColumn[] = [ - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'Stake', - accessorKey: 'stake', - }, - { - header: 'Proposed next Epoch gas price', - accessorKey: 'nextEpochGasPrice', - }, - { - header: 'APY', - accessorKey: 'apy', - }, - { - header: 'Commission', - accessorKey: 'commission', - }, - { - header: 'Last Epoch Reward', - accessorKey: 'lastReward', - }, - { - header: 'Voting Power', - accessorKey: 'votingPower', - }, - { - header: 'Status', - accessorKey: 'atRisk', - }, -]; + +function IotaStats({ + amount, + showSign, + ...props +}: Omit<StatsProps, 'children'> & { + amount: bigint | number | string | undefined | null; + showSign?: boolean; +}): JSX.Element { + const [formattedAmount, symbol] = useFormatCoin( + amount, + IOTA_TYPE_ARG, + CoinFormat.ROUNDED, + showSign, + ); + + return ( + <Stats postfix={formattedAmount && symbol} {...props}> + {formattedAmount || '--'} + </Stats> + ); +} enum EpochTabs { Checkpoints = 'checkpoints', @@ -90,17 +74,15 @@ export default function EpochDetail() { ); const validatorsTable = useMemo(() => { - if (!epochData?.validators || epochData.validators.length === 0) return null; + if (!epochData?.validators) return null; // todo: enrich this historical validator data when we have // at-risk / pending validators for historical epochs - return generateValidatorsTableData({ - validators: [...epochData.validators].sort(() => 0.5 - Math.random()), - atRiskValidators: [], - validatorEvents: [], - rollingAverageApys: null, - columns: VALIDATOR_COLUMNS, - showValidatorIcon: true, - }); + return validatorsTableData( + [...epochData.validators].sort(() => 0.5 - Math.random()), + [], + [], + null, + ); }, [epochData]); if (isPending) return <PageLayout content={<LoadingIndicator />} />; @@ -129,60 +111,51 @@ export default function EpochDetail() { <PageLayout content={ <div className="flex flex-col space-y-16"> - <div - className={cx( - 'grid grid-cols-1 gap-md--rs', - isCurrentEpoch ? 'md:grid-cols-2' : 'md:grid-cols-3', - )} - > - <EpochStats - title={`Epoch ${epochData.epoch}`} - subtitle={isCurrentEpoch ? 'In progress' : 'Ended'} - > - <EpochTopStats + <div className="grid grid-flow-row gap-4 sm:gap-2 md:flex md:gap-6"> + <div className="flex min-w-[136px] max-w-[240px]"> + <EpochProgress + epoch={epochData.epoch} inProgress={isCurrentEpoch} start={Number(epochData.epochStartTimestamp)} end={Number(epochData.endOfEpochInfo?.epochEndTimestamp ?? 0)} - endOfEpochInfo={epochData.endOfEpochInfo} /> + </div> + + <EpochStats label="Rewards"> + <IotaStats + label="Total Stake" + tooltip="" + amount={epochData.endOfEpochInfo?.totalStake} + /> + <IotaStats + label="Stake Rewards" + amount={epochData.endOfEpochInfo?.totalStakeRewardsDistributed} + /> + <IotaStats + label="Gas Fees" + amount={epochData.endOfEpochInfo?.totalGasFees} + /> + </EpochStats> + + <EpochStats label="Storage Fund Balance"> + <IotaStats + label="Fund Size" + amount={epochData.endOfEpochInfo?.storageFundBalance} + /> + <IotaStats label="Net Inflow" amount={netInflow} /> + <IotaStats label="Fund Inflow" amount={fundInflow} /> + <IotaStats label="Fund Outflow" amount={fundOutflow} /> </EpochStats> - {!isCurrentEpoch && ( - <> - <EpochStats title="Rewards"> - <EpochStatsGrid> - <TokenStats - label="Total Stake" - amount={epochData.endOfEpochInfo?.totalStake} - /> - <TokenStats - label="Stake Rewards" - amount={ - epochData.endOfEpochInfo - ?.totalStakeRewardsDistributed - } - /> - <TokenStats - label="Gas Fees" - amount={epochData.endOfEpochInfo?.totalGasFees} - /> - </EpochStatsGrid> - </EpochStats> - - <EpochStats title="Storage Fund Balance"> - <EpochStatsGrid> - <TokenStats - label="Fund Size" - amount={epochData.endOfEpochInfo?.storageFundBalance} - /> - <TokenStats label="Net Inflow" amount={netInflow} /> - <TokenStats label="Fund Inflow" amount={fundInflow} /> - <TokenStats label="Fund Outflow" amount={fundOutflow} /> - </EpochStatsGrid> - </EpochStats> - </> - )} - - {isCurrentEpoch && <ValidatorStatus />} + + <EpochStats label="Supply"> + <IotaStats + label="Supply Change" + amount={getSupplyChangeAfterEpochEnd(epochData.endOfEpochInfo)} + showSign + /> + </EpochStats> + + {isCurrentEpoch ? <ValidatorStatus /> : null} </div> <div className="rounded-xl bg-white"> @@ -218,6 +191,7 @@ export default function EpochDetail() { <TableCard data={validatorsTable.data} columns={validatorsTable.columns} + sortTable /> ) : null} </div> diff --git a/apps/explorer/src/pages/epochs/stats/EpochProgress.tsx b/apps/explorer/src/pages/epochs/stats/EpochProgress.tsx new file mode 100644 index 00000000000..7d58416f325 --- /dev/null +++ b/apps/explorer/src/pages/epochs/stats/EpochProgress.tsx @@ -0,0 +1,68 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { formatDate } from '@iota/core'; +import { Heading, Text } from '@iota/ui'; +import clsx from 'clsx'; + +import { Card, ProgressBar } from '~/components/ui'; +import { getElapsedTime, useEpochProgress } from '~/pages/epochs/utils'; + +interface EpochProgressProps { + epoch?: string; + start: number; + end?: number; + inProgress?: boolean; +} + +export function EpochProgress({ epoch, start, end, inProgress }: EpochProgressProps): JSX.Element { + const { progress, label } = useEpochProgress(); + + const elapsedTime = !inProgress && start && end ? getElapsedTime(start, end) : undefined; + + return ( + <Card bg={inProgress ? 'highlight' : 'default'} spacing="lg" rounded="2xl"> + <div className="flex flex-col space-y-12"> + <div className={clsx(inProgress ? 'space-y-4' : 'space-y-6')}> + <div className="flex flex-col gap-2"> + <Heading color="steel-darker" variant="heading3/semibold"> + {inProgress ? `Epoch ${epoch} in progress` : `Epoch ${epoch}`} + </Heading> + {elapsedTime && ( + <Heading variant="heading6/medium" color="steel-darker"> + {elapsedTime} + </Heading> + )} + </div> + <div> + <Text variant="pSubtitleSmall/normal" uppercase color="steel-darker"> + Start + </Text> + <Text variant="pSubtitle/semibold" color="steel-darker"> + {formatDate(start)} + </Text> + </div> + {!inProgress && end ? ( + <div> + <Text variant="pSubtitleSmall/normal" uppercase color="steel-darker"> + End + </Text> + <Text variant="pSubtitle/semibold" color="steel-darker"> + {formatDate(end)} + </Text> + </div> + ) : null} + </div> + {inProgress ? ( + <div className="space-y-2"> + <Heading variant="heading6/medium" color="steel-darker"> + {label} + </Heading> + <ProgressBar progress={progress || 0} /> + </div> + ) : null} + </div> + </Card> + ); +} diff --git a/apps/explorer/src/pages/epochs/stats/EpochStats.tsx b/apps/explorer/src/pages/epochs/stats/EpochStats.tsx index 33c15f3a31d..625503835c3 100644 --- a/apps/explorer/src/pages/epochs/stats/EpochStats.tsx +++ b/apps/explorer/src/pages/epochs/stats/EpochStats.tsx @@ -2,24 +2,27 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { Panel, Title } from '@iota/apps-ui-kit'; -import type { ComponentProps } from 'react'; +import { Heading } from '@iota/ui'; +import { type ReactNode } from 'react'; -type TitleProps = ComponentProps<typeof Title>; -export function EpochStats({ - children, - ...titleProps -}: React.PropsWithChildren<TitleProps>): JSX.Element { +import { Card } from '~/components/ui'; + +interface EpochStatsProps { + label: string; + children: ReactNode; +} + +export function EpochStats({ label, children }: EpochStatsProps): JSX.Element { return ( - <Panel> - <div className="flex flex-col"> - {titleProps && <Title {...titleProps} />} - <div className="w-full p-md--rs">{children}</div> + <Card spacing="lg" rounded="2xl"> + <div className="flex flex-col gap-8"> + {label && ( + <Heading color="steel-darker" variant="heading4/semibold"> + {label} + </Heading> + )} + <div className="grid grid-cols-2 gap-8">{children}</div> </div> - </Panel> + </Card> ); } - -export function EpochStatsGrid({ children }: React.PropsWithChildren): React.JSX.Element { - return <div className="grid w-full grid-cols-1 gap-md--rs md:grid-cols-2">{children}</div>; -} diff --git a/apps/explorer/src/pages/epochs/stats/EpochTopStats.tsx b/apps/explorer/src/pages/epochs/stats/EpochTopStats.tsx deleted file mode 100644 index c99f5a51c39..00000000000 --- a/apps/explorer/src/pages/epochs/stats/EpochTopStats.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { ProgressBar } from '~/components'; -import { EpochStatsGrid } from './EpochStats'; -import { LabelText, LabelTextSize } from '@iota/apps-ui-kit'; -import { formatDate } from '@iota/core'; -import { TokenStats } from './TokenStats'; -import { getSupplyChangeAfterEpochEnd } from '~/lib'; -import { useEpochProgress } from '../utils'; -import type { EndOfEpochInfo } from '@iota/iota-sdk/src/client'; - -interface EpochProgressProps { - start: number; - end?: number; - inProgress?: boolean; - endOfEpochInfo?: EndOfEpochInfo | null; -} - -export function EpochTopStats({ - start, - end, - inProgress, - endOfEpochInfo, -}: EpochProgressProps): React.JSX.Element { - const { progress, label } = useEpochProgress(); - const endTime = inProgress ? label : end ? formatDate(end) : undefined; - - return ( - <div className="flex w-full flex-col gap-md--rs"> - {inProgress ? <ProgressBar progress={progress || 0} /> : null} - - <EpochStatsGrid> - <LabelText text={formatDate(start)} label="Start" /> - {endTime ? <LabelText text={endTime} label="End" /> : null} - {endOfEpochInfo && ( - <TokenStats - label="Supply Change" - size={LabelTextSize.Large} - amount={getSupplyChangeAfterEpochEnd(endOfEpochInfo)} - showSign - /> - )} - </EpochStatsGrid> - </div> - ); -} diff --git a/apps/explorer/src/pages/epochs/stats/TokenStats.tsx b/apps/explorer/src/pages/epochs/stats/TokenStats.tsx deleted file mode 100644 index b7e0daa1061..00000000000 --- a/apps/explorer/src/pages/epochs/stats/TokenStats.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { LabelText, LabelTextSize } from '@iota/apps-ui-kit'; -import { CoinFormat, useFormatCoin } from '@iota/core'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; - -type LabelTextProps = Omit<React.ComponentProps<typeof LabelText>, 'text' | 'size'>; - -interface TokenStatsProps extends LabelTextProps { - amount: bigint | number | string | undefined | null; - showSign?: boolean; - size?: LabelTextSize; -} - -export function TokenStats({ - amount, - showSign, - size = LabelTextSize.Large, - ...props -}: TokenStatsProps): React.JSX.Element { - const [formattedAmount, symbol] = useFormatCoin( - amount, - IOTA_TYPE_ARG, - CoinFormat.ROUNDED, - showSign, - ); - - return <LabelText text={formattedAmount} supportingLabel={symbol} size={size} {...props} />; -} diff --git a/apps/explorer/src/pages/epochs/stats/ValidatorStatus.tsx b/apps/explorer/src/pages/epochs/stats/ValidatorStatus.tsx index 449029b3217..3131336cb3a 100644 --- a/apps/explorer/src/pages/epochs/stats/ValidatorStatus.tsx +++ b/apps/explorer/src/pages/epochs/stats/ValidatorStatus.tsx @@ -2,12 +2,12 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { DisplayStats, IOTA_PRIMITIVES_COLOR_PALETTE, Panel, Title } from '@iota/apps-ui-kit'; import { getRefGasPrice } from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; +import { Heading, Text } from '@iota/ui'; import { useMemo } from 'react'; -import { RingChart, RingChartLegend } from '~/components/ui'; +import { Card, RingChart, RingChartLegend } from '~/components/ui'; export function ValidatorStatus(): JSX.Element | null { const { data } = useIotaClientQuery('getLatestIotaSystemState'); @@ -21,9 +21,6 @@ export function ValidatorStatus(): JSX.Element | null { const nextEpoch = Number(data.epoch || 0) + 1; - const getHexColorWithOpacity = (color: string, opacity: number) => - `${color}${Math.round(opacity * 255).toString(16)}`; - const chartData = [ { value: data.activeValidators.length, @@ -31,47 +28,53 @@ export function ValidatorStatus(): JSX.Element | null { gradient: { deg: 315, values: [ - { percent: 0, color: IOTA_PRIMITIVES_COLOR_PALETTE.primary[30] }, - { percent: 100, color: IOTA_PRIMITIVES_COLOR_PALETTE.primary[30] }, + { percent: 0, color: '#4C75A6' }, + { percent: 100, color: '#589AEA' }, ], }, }, { value: Number(data.pendingActiveValidatorsSize ?? 0), label: 'New', - color: getHexColorWithOpacity(IOTA_PRIMITIVES_COLOR_PALETTE.primary[30], 0.6), + color: '#F2BD24', }, { value: data.atRiskValidators.length, label: 'At Risk', - color: IOTA_PRIMITIVES_COLOR_PALETTE.neutral[90], + color: '#FF794B', }, ]; return ( - <Panel> - <div className="flex flex-col"> - <Title title={`Validators in Epoch ${nextEpoch}`} /> - <div className="flex flex-col items-start justify-center gap-x-xl gap-y-sm p-md--rs md:flex-row md:items-center md:justify-between md:gap-sm--rs"> - <div className="flex w-auto flex-row gap-x-md p-md md:max-w-[50%]"> - <div className="h-[92px] w-[92px]"> - <RingChart data={chartData} width={4} /> - </div> - <div className="flex flex-col items-center justify-center gap-xs lg:items-start"> - <RingChartLegend data={chartData} /> - </div> - </div> + <Card spacing="lg" bg="white" border="steel" rounded="2xl"> + <div className="flex items-center gap-5"> + <div className="min-h-[96px] min-w-[96px]"> + <RingChart data={chartData} /> + </div> + + <div className="self-start"> + <RingChartLegend data={chartData} title={`Validators in Epoch ${nextEpoch}`} /> + </div> + </div> - <div className="h-full w-full max-w-[250px] sm:w-1/2 md:w-auto lg:w-1/2 "> - <DisplayStats - label="Estimated Next Epoch - Reference Gas Price" - value={nextRefGasPrice.toString()} - supportingLabel="nano" - /> - </div> + <div className="mt-8 flex items-center justify-between rounded-lg border border-solid border-steel px-3 py-2"> + <div> + <Text variant="pSubtitle/semibold" color="steel-darker"> + Estimated Next Epoch + </Text> + <Text variant="pSubtitle/semibold" color="steel-darker"> + Reference Gas Price + </Text> + </div> + <div className="text-right"> + <Heading variant="heading4/semibold" color="steel-darker"> + {nextRefGasPrice.toString()} + </Heading> + <Text variant="pBody/medium" color="steel-darker"> + nano + </Text> </div> </div> - </Panel> + </Card> ); } diff --git a/apps/explorer/src/pages/id-page/index.tsx b/apps/explorer/src/pages/id-page/index.tsx index 4b62dc99ece..b6023de5ec3 100644 --- a/apps/explorer/src/pages/id-page/index.tsx +++ b/apps/explorer/src/pages/id-page/index.tsx @@ -2,30 +2,44 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { + isIotaNSName, + useGetObject, + useResolveIotaNSAddress, + useResolveIotaNSName, +} from '@iota/core'; import { ObjectDetailsHeader } from '@iota/icons'; import { useParams } from 'react-router-dom'; -import { ErrorBoundary, PageHeader, PageLayout } from '~/components'; +import { ErrorBoundary, PageLayout } from '~/components'; +import { TotalStaked } from '~/pages/address-result/TotalStaked'; import { PageContent } from '~/pages/id-page/PageContent'; -import { ObjectView } from '../object-result/views/ObjectView'; -import { TotalStaked } from '../address-result/TotalStaked'; -import { useGetObject } from '@iota/core'; +import { ObjectView } from '~/pages/object-result/views/ObjectView'; +import { PageHeader } from '~/components/ui'; interface HeaderProps { address: string; + loading?: boolean; + error?: Error | null; } -function Header({ address }: HeaderProps): JSX.Element { +function Header({ address, loading, error }: HeaderProps): JSX.Element { + const { + data: domainName, + isLoading, + error: resolveIotansError, + } = useResolveIotaNSName(address); const { data, isPending, error: getObjectError } = useGetObject(address!); const isObject = !!data?.data; - const errorText = getObjectError?.message; + const errorText = getObjectError?.message ?? resolveIotansError?.message ?? error?.message; return ( <div> <PageHeader - loading={isPending} error={errorText} + loading={loading || isLoading || isPending} type={isObject ? 'Object' : 'Address'} title={address} + subtitle={domainName} before={<ObjectDetailsHeader className="h-6 w-6" />} after={<TotalStaked address={address} />} /> @@ -46,12 +60,21 @@ interface PageLayoutContainerProps { } function PageLayoutContainer({ address }: PageLayoutContainerProps): JSX.Element { + const { id } = useParams(); + const isIotaNSAddress = isIotaNSName(id!); + const { + data, + isLoading, + error: iotansAddressError, + } = useResolveIotaNSAddress(address, isIotaNSAddress); + return ( <PageLayout + loading={isLoading} content={ <> <Header address={address} /> - <PageContent address={address} /> + <PageContent address={data || address} error={iotansAddressError} /> </> } /> diff --git a/apps/explorer/src/pages/object-result/views/ObjectView.tsx b/apps/explorer/src/pages/object-result/views/ObjectView.tsx index af34eb65f7d..8be0412c048 100644 --- a/apps/explorer/src/pages/object-result/views/ObjectView.tsx +++ b/apps/explorer/src/pages/object-result/views/ObjectView.tsx @@ -147,7 +147,6 @@ function LastTxBlockCard({ digest }: LastTxBlockCardProps): JSX.Element { /> ); } - interface OwnerCardProps { objOwner: ObjectOwner; } diff --git a/apps/explorer/src/pages/transaction-result/transaction-summary/BalanceChanges.tsx b/apps/explorer/src/pages/transaction-result/transaction-summary/BalanceChanges.tsx index 97c7c3e8e6f..a505a6731b4 100644 --- a/apps/explorer/src/pages/transaction-result/transaction-summary/BalanceChanges.tsx +++ b/apps/explorer/src/pages/transaction-result/transaction-summary/BalanceChanges.tsx @@ -9,6 +9,7 @@ import { getRecognizedUnRecognizedTokenChanges, useCoinMetadata, useFormatCoin, + useResolveIotaNSName, } from '@iota/core'; import { Heading, Text } from '@iota/ui'; import clsx from 'clsx'; @@ -72,6 +73,7 @@ function BalanceChangeEntry({ change }: { change: BalanceChange }): JSX.Element } function BalanceChangeCard({ changes, owner }: { changes: BalanceChange[]; owner: string }) { + const { data: iotansDomainName } = useResolveIotaNSName(owner); const { recognizedTokenChanges, unRecognizedTokenChanges } = useMemo( () => getRecognizedUnRecognizedTokenChanges(changes), [changes], @@ -95,7 +97,7 @@ function BalanceChangeCard({ changes, owner }: { changes: BalanceChange[]; owner Owner </Text> <Text variant="pBody/medium" color="hero-dark"> - <AddressLink address={owner} /> + <AddressLink label={iotansDomainName || undefined} address={owner} /> </Text> </div> ) : null diff --git a/apps/explorer/src/pages/transaction-result/transaction-summary/ObjectChanges.tsx b/apps/explorer/src/pages/transaction-result/transaction-summary/ObjectChanges.tsx index e9ea591b4c8..739711f892d 100644 --- a/apps/explorer/src/pages/transaction-result/transaction-summary/ObjectChanges.tsx +++ b/apps/explorer/src/pages/transaction-result/transaction-summary/ObjectChanges.tsx @@ -4,6 +4,7 @@ import { ObjectChangeLabels, + useResolveIotaNSName, type IotaObjectChangeTypes, type IotaObjectChangeWithDisplay, type ObjectChangesByOwner, @@ -248,13 +249,17 @@ function ObjectChangeEntriesCardFooter({ ownerType, ownerAddress, }: ObjectChangeEntriesCardFooterProps): JSX.Element { + const { data: iotansDomainName } = useResolveIotaNSName(ownerAddress); + return ( <div className="flex flex-wrap items-center justify-between"> <Text variant="pBody/medium" color="steel-dark"> Owner </Text> - {ownerType === 'AddressOwner' && <AddressLink address={ownerAddress} />} + {ownerType === 'AddressOwner' && ( + <AddressLink label={iotansDomainName || undefined} address={ownerAddress} /> + )} {ownerType === 'ObjectOwner' && <ObjectLink objectId={ownerAddress} />} diff --git a/apps/explorer/src/pages/transaction-result/transaction-summary/TransactionDetails.tsx b/apps/explorer/src/pages/transaction-result/transaction-summary/TransactionDetails.tsx index 8261528fd21..a90d9f1cd3c 100644 --- a/apps/explorer/src/pages/transaction-result/transaction-summary/TransactionDetails.tsx +++ b/apps/explorer/src/pages/transaction-result/transaction-summary/TransactionDetails.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 import { DisplayStats } from '@iota/apps-ui-kit'; -import { formatDate } from '@iota/core'; +import { formatDate, useResolveIotaNSName } from '@iota/core'; interface TransactionDetailsProps { sender?: string; @@ -18,13 +18,15 @@ export function TransactionDetails({ executedEpoch, timestamp, }: TransactionDetailsProps): JSX.Element { + const { data: domainName } = useResolveIotaNSName(sender); + return ( <div className="grid grid-cols-1 gap-sm md:grid-cols-4"> {sender && ( <DisplayStats label="Sender" value={sender} - valueLink={`/address/${sender}`} + valueLink={domainName ?? `/address/${sender}`} isTruncated /> )} diff --git a/apps/explorer/src/pages/validators/Validators.tsx b/apps/explorer/src/pages/validators/Validators.tsx index b3874e85d74..a7607c1647a 100644 --- a/apps/explorer/src/pages/validators/Validators.tsx +++ b/apps/explorer/src/pages/validators/Validators.tsx @@ -2,28 +2,243 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import React, { type JSX, useMemo } from 'react'; -import { roundFloat, useFormatCoin, useGetValidatorsApy, useGetValidatorsEvents } from '@iota/core'; +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { DisplayStats, DisplayStatsSize, DisplayStatsType, TooltipPosition, } from '@iota/apps-ui-kit'; +import { + formatPercentageDisplay, + roundFloat, + useFormatCoin, + useGetValidatorsApy, + useGetValidatorsEvents, + type ApyByValidator, +} from '@iota/core'; import { useIotaClientQuery } from '@iota/dapp-kit'; +import { type IotaEvent, type IotaValidatorSummary } from '@iota/iota-sdk/client'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { Text } from '@iota/ui'; +import { useMemo } from 'react'; +import { ErrorBoundary, PageLayout, StakeColumn } from '~/components'; import { - ErrorBoundary, - PageLayout, Banner, + ImageIcon, + Link, PlaceholderTable, TableCard, TableHeader, -} from '~/components'; -import { generateValidatorsTableData } from '~/lib/ui/utils/generateValidatorsTableData'; + Tooltip, +} from '~/components/ui'; +import { VALIDATOR_LOW_STAKE_GRACE_PERIOD } from '~/lib/constants'; +import { ampli, getValidatorMoveEvent } from '~/lib/utils'; + +export function validatorsTableData( + validators: IotaValidatorSummary[], + atRiskValidators: [string, string][], + validatorEvents: IotaEvent[], + rollingAverageApys: ApyByValidator | null, +) { + return { + data: [...validators] + .sort(() => 0.5 - Math.random()) + .map((validator) => { + const validatorName = validator.name; + const totalStake = validator.stakingPoolIotaBalance; + const img = validator.imageUrl; + + const event = getValidatorMoveEvent(validatorEvents, validator.iotaAddress) as { + pool_staking_reward?: string; + }; + + const atRiskValidator = atRiskValidators.find( + ([address]) => address === validator.iotaAddress, + ); + const isAtRisk = !!atRiskValidator; + const lastReward = event?.pool_staking_reward ?? null; + const { apy, isApyApproxZero } = rollingAverageApys?.[validator.iotaAddress] ?? { + apy: null, + }; + + return { + name: { + name: validatorName, + logo: validator.imageUrl, + }, + stake: totalStake, + apy: { + apy, + isApyApproxZero, + }, + nextEpochGasPrice: validator.nextEpochGasPrice, + commission: Number(validator.commissionRate) / 100, + img: img, + address: validator.iotaAddress, + lastReward: lastReward ?? null, + votingPower: Number(validator.votingPower) / 100, + atRisk: isAtRisk + ? VALIDATOR_LOW_STAKE_GRACE_PERIOD - Number(atRiskValidator[1]) + : null, + }; + }), + columns: [ + { + header: '#', + accessorKey: 'number', + cell: (props: any) => ( + <Text variant="bodySmall/medium" color="steel-dark"> + {props.table.getSortedRowModel().flatRows.indexOf(props.row) + 1} + </Text> + ), + }, + { + header: 'Name', + accessorKey: 'name', + enableSorting: true, + sortingFn: (a: any, b: any, colId: string) => + a.getValue(colId).name.localeCompare(b.getValue(colId).name, 'en', { + sensitivity: 'base', + numeric: true, + }), + cell: (props: any) => { + const { name, logo } = props.getValue(); + return ( + <Link + to={`/validator/${encodeURIComponent(props.row.original.address)}`} + onClick={() => + ampli.clickedValidatorRow({ + sourceFlow: 'Epoch details', + validatorAddress: props.row.original.address, + validatorName: name, + }) + } + > + <div className="flex items-center gap-2.5"> + <ImageIcon + src={logo} + size="sm" + label={name} + fallback={name} + circle + /> + <Text variant="bodySmall/medium" color="steel-darker"> + {name} + </Text> + </div> + </Link> + ); + }, + }, + { + header: 'Stake', + accessorKey: 'stake', + enableSorting: true, + cell: (props: any) => <StakeColumn stake={props.getValue()} />, + }, + { + header: 'Proposed Next Epoch Gas Price', + accessorKey: 'nextEpochGasPrice', + enableSorting: true, + cell: (props: any) => <StakeColumn stake={props.getValue()} inNano />, + }, + { + header: 'APY', + accessorKey: 'apy', + enableSorting: true, + sortingFn: (a: any, b: any, colId: string) => + a.getValue(colId)?.apy < b.getValue(colId)?.apy ? -1 : 1, + cell: (props: any) => { + const { apy, isApyApproxZero } = props.getValue(); + return ( + <Text variant="bodySmall/medium" color="steel-darker"> + {formatPercentageDisplay(apy, '--', isApyApproxZero)} + </Text> + ); + }, + }, + { + header: 'Commission', + accessorKey: 'commission', + enableSorting: true, + cell: (props: any) => { + const commissionRate = props.getValue(); + return ( + <Text variant="bodySmall/medium" color="steel-darker"> + {commissionRate}% + </Text> + ); + }, + }, + { + header: 'Last Epoch Rewards', + accessorKey: 'lastReward', + enableSorting: true, + cell: (props: any) => { + const lastReward = props.getValue(); + return lastReward !== null ? ( + <StakeColumn stake={Number(lastReward)} /> + ) : ( + <Text variant="bodySmall/medium" color="steel-darker"> + -- + </Text> + ); + }, + }, + { + header: 'Voting Power', + accessorKey: 'votingPower', + enableSorting: true, + cell: (props: any) => { + const votingPower = props.getValue(); + return ( + <Text variant="bodySmall/medium" color="steel-darker"> + {votingPower}% + </Text> + ); + }, + }, + { + header: 'Status', + accessorKey: 'atRisk', + cell: (props: any) => { + const atRisk = props.getValue(); + const label = 'At Risk'; + return atRisk !== null ? ( + <Tooltip + tip="Staked IOTA is below the minimum IOTA stake threshold to remain a validator." + onOpen={() => + ampli.activatedTooltip({ + tooltipLabel: label, + }) + } + > + <div className="flex cursor-pointer flex-nowrap items-center"> + <Text color="issue" variant="bodySmall/medium"> + {label} + </Text> +   + <Text uppercase variant="bodySmall/medium" color="steel-dark"> + {atRisk > 1 ? `in ${atRisk} epochs` : 'next epoch'} + </Text> + </div> + </Tooltip> + ) : ( + <Text variant="bodySmall/medium" color="steel-darker"> + Active + </Text> + ); + }, + }, + ], + }; +} function ValidatorPageResult(): JSX.Element { const { data, isPending, isSuccess, isError } = useIotaClientQuery('getLatestIotaSystemState'); + const numberOfValidators = data?.activeValidators.length || 0; const { @@ -74,12 +289,12 @@ function ValidatorPageResult(): JSX.Element { const validatorsTable = useMemo(() => { if (!data || !validatorEvents) return null; - return generateValidatorsTableData({ - validators: [...data.activeValidators].sort(() => 0.5 - Math.random()), - atRiskValidators: data.atRiskValidators, + return validatorsTableData( + data.activeValidators, + data.atRiskValidators, validatorEvents, - rollingAverageApys: validatorsApy || null, - }); + validatorsApy || null, + ); }, [data, validatorEvents, validatorsApy]); const [formattedTotalStakedAmount, totalStakedSymbol] = useFormatCoin( @@ -155,7 +370,7 @@ function ValidatorPageResult(): JSX.Element { <TableCard data={validatorsTable.data} columns={validatorsTable.columns} - areHeadersCentered={false} + sortTable /> )} </ErrorBoundary> diff --git a/apps/ui-kit/src/lib/components/atoms/label-text/LabelText.tsx b/apps/ui-kit/src/lib/components/atoms/label-text/LabelText.tsx index 5e7f8247813..f5c5c245f46 100644 --- a/apps/ui-kit/src/lib/components/atoms/label-text/LabelText.tsx +++ b/apps/ui-kit/src/lib/components/atoms/label-text/LabelText.tsx @@ -12,7 +12,7 @@ interface LabelTextProps { /** * The size of the LabelText. */ - size?: LabelTextSize; + size: LabelTextSize; /** * The position of the LabelText. */ @@ -25,6 +25,10 @@ interface LabelTextProps { * The text of the LabelText. */ label: string; + /** + * Show the supporting label. + */ + showSupportingLabel: boolean; /** * The text of the LabelText. */ @@ -40,10 +44,11 @@ interface LabelTextProps { } export function LabelText({ - size = LabelTextSize.Medium, + size, isCentered, supportingLabel, label, + showSupportingLabel, text, tooltipPosition, tooltipText, @@ -61,7 +66,7 @@ export function LabelText({ > {text} </span> - {supportingLabel && ( + {showSupportingLabel && supportingLabel && ( <span className={cx( 'font-inter text-neutral-60 dark:text-neutral-40', diff --git a/apps/ui-kit/src/lib/components/atoms/list-item/ListItem.tsx b/apps/ui-kit/src/lib/components/atoms/list-item/ListItem.tsx index 2a5806cf08f..5e8028cdb28 100644 --- a/apps/ui-kit/src/lib/components/atoms/list-item/ListItem.tsx +++ b/apps/ui-kit/src/lib/components/atoms/list-item/ListItem.tsx @@ -33,14 +33,8 @@ export function ListItem({ children, }: PropsWithChildren<ListItemProps>): React.JSX.Element { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { - if ((event.key === 'Enter' || event.key === ' ') && !isDisabled && onClick) { - onClick(); - } - } - - function handleClick() { - if (!isDisabled && onClick) { - onClick(); + if (event.key === 'Enter' || event.key === ' ') { + onClick && onClick(); } } @@ -56,14 +50,13 @@ export function ListItem({ )} > <div - onClick={handleClick} + onClick={onClick} role="button" tabIndex={0} onKeyDown={handleKeyDown} className={cx( 'relative flex flex-row items-center justify-between px-md py-sm text-neutral-10 dark:text-neutral-92', - !isDisabled && onClick ? 'cursor-pointer' : 'cursor-default', - { 'state-layer': !isDisabled }, + { 'state-layer': !isDisabled, 'cursor-pointer': !isDisabled && onClick }, )} > {children} diff --git a/apps/ui-kit/src/lib/components/molecules/chip/Chip.tsx b/apps/ui-kit/src/lib/components/molecules/chip/Chip.tsx index 9f7a2ebff7d..d98ea4381c3 100644 --- a/apps/ui-kit/src/lib/components/molecules/chip/Chip.tsx +++ b/apps/ui-kit/src/lib/components/molecules/chip/Chip.tsx @@ -31,38 +31,20 @@ interface ChipProps { * Callback when the close icon is clicked */ onClose?: () => void; - /** - * On Click handler for the chip - */ - onClick?: () => void; /** * Avatar to show in the chip. */ avatar?: React.JSX.Element; /** - * Leading element to show in the chip. - */ - leadingElement?: React.JSX.Element; - /** - * Trailing element to show in the chip. + * Icon to show in the chip. */ - trailingElement?: React.JSX.Element; + icon?: React.JSX.Element; } -export function Chip({ - label, - showClose, - selected, - onClose, - onClick, - avatar, - leadingElement, - trailingElement, -}: ChipProps) { +export function Chip({ label, showClose, selected, onClose, avatar, icon }: ChipProps) { const chipState = selected ? ChipState.Selected : ChipState.Default; return ( <ButtonUnstyled - onClick={onClick} className={cx( 'border', ROUNDED_CLASS, @@ -75,16 +57,15 @@ export function Chip({ className={cx( 'flex h-full w-full flex-row items-center gap-x-2', avatar ? 'py-xxs' : 'py-[6px]', - avatar ? 'pl-xxs' : leadingElement ? 'pl-xs' : 'pl-sm', + avatar ? 'pl-xxs' : icon ? 'pl-xs' : 'pl-sm', ROUNDED_CLASS, STATE_LAYER_CLASSES, showClose ? 'pr-xs' : 'pr-sm', TEXT_COLOR[chipState], )} > - {avatar ?? leadingElement} + {avatar ?? icon} <span className="text-body-md">{label}</span> - {trailingElement} {showClose && ( <ButtonUnstyled onClick={onClose} diff --git a/apps/ui-kit/src/lib/components/molecules/display-stats/DisplayStats.tsx b/apps/ui-kit/src/lib/components/molecules/display-stats/DisplayStats.tsx index 53fbac6a7ad..a33f350c637 100644 --- a/apps/ui-kit/src/lib/components/molecules/display-stats/DisplayStats.tsx +++ b/apps/ui-kit/src/lib/components/molecules/display-stats/DisplayStats.tsx @@ -101,7 +101,7 @@ export function DisplayStats({ })} > <div className="flex flex-row items-center gap-xxs"> - <span className={cx(labelTextClass, 'whitespace-pre-line')}>{label}</span> + <span className={cx(labelTextClass)}>{label}</span> {tooltipText && ( <Tooltip text={tooltipText} position={tooltipPosition}> <Info className="opacity-40" /> diff --git a/apps/ui-kit/src/lib/components/molecules/dropdown/dropdown-position.enum.ts b/apps/ui-kit/src/lib/components/molecules/dropdown/dropdown-position.enum.ts deleted file mode 100644 index 9920123fa5d..00000000000 --- a/apps/ui-kit/src/lib/components/molecules/dropdown/dropdown-position.enum.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -export enum DropdownPosition { - Top = 'top', - Bottom = 'bottom', -} diff --git a/apps/ui-kit/src/lib/components/molecules/dropdown/index.ts b/apps/ui-kit/src/lib/components/molecules/dropdown/index.ts index 7661c872580..cdeac2b5132 100644 --- a/apps/ui-kit/src/lib/components/molecules/dropdown/index.ts +++ b/apps/ui-kit/src/lib/components/molecules/dropdown/index.ts @@ -2,4 +2,3 @@ // SPDX-License-Identifier: Apache-2.0 export * from './Dropdown'; -export * from './dropdown-position.enum'; diff --git a/apps/ui-kit/src/lib/components/molecules/select/Select.tsx b/apps/ui-kit/src/lib/components/molecules/select/Select.tsx index 7ddc5ec8472..3b35da5bfc4 100644 --- a/apps/ui-kit/src/lib/components/molecules/select/Select.tsx +++ b/apps/ui-kit/src/lib/components/molecules/select/Select.tsx @@ -9,7 +9,6 @@ import { SecondaryText } from '../../atoms/secondary-text'; import { InputWrapper, LabelHtmlTag } from '../input/InputWrapper'; import { ButtonUnstyled } from '../../atoms/button'; import { ListItem } from '../../atoms'; -import { DropdownPosition } from '../dropdown'; export type SelectOption = | string @@ -57,10 +56,6 @@ interface SelectProps extends Pick<React.HTMLProps<HTMLSelectElement>, 'disabled * The callback to call when the option is clicked. */ onOptionClick?: (id: string) => void; - /** - * The dropdown position - */ - dropdownPosition?: DropdownPosition; } export const Select = forwardRef<HTMLButtonElement, SelectProps>( @@ -77,7 +72,6 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>( onValueChange, onOptionClick, value, - dropdownPosition = DropdownPosition.Bottom, }, ref, ) => { @@ -170,11 +164,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>( /> )} <div - className={cx('absolute z-50 min-w-full', { + className={cx('absolute top-full z-50 min-w-full', { hidden: !isOpen, - 'top-full': - !dropdownPosition || dropdownPosition === DropdownPosition.Bottom, - 'bottom-full': dropdownPosition === DropdownPosition.Top, })} > <Dropdown> diff --git a/apps/ui-kit/src/lib/components/molecules/table-cell/TableCell.tsx b/apps/ui-kit/src/lib/components/molecules/table-cell/TableCell.tsx index 19d9045c2f6..f993416d101 100644 --- a/apps/ui-kit/src/lib/components/molecules/table-cell/TableCell.tsx +++ b/apps/ui-kit/src/lib/components/molecules/table-cell/TableCell.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; import { BadgeType, Badge, Checkbox, ButtonUnstyled } from '../../atoms'; -import { TableCellType, TableCellTextColor } from './table-cell.enums'; +import { TableCellType } from './table-cell.enums'; import { Copy } from '@iota/ui-icons'; import cx from 'classnames'; interface TableCellBaseProps { @@ -18,14 +18,6 @@ interface TableCellBaseProps { * Whether the cell content should be centered. */ isContentCentered?: boolean; - /** - * The color of the text. - */ - textColor?: TableCellTextColor; - /** - * Whether to not wrap the text in the cell. - */ - noWrap?: boolean; } type TableCellText = { @@ -121,17 +113,6 @@ type TableCellLink = { isExternal?: boolean; }; -type TableCellChildren = { - /** - * The type of the cell. - */ - type: TableCellType.Children; - /** - * The children of the cell. - */ - children?: React.ReactNode; -}; - export type TableCellProps = TableCellBaseProps & ( | TableCellText @@ -141,18 +122,12 @@ export type TableCellProps = TableCellBaseProps & | TableCellCheckbox | TableCellPlaceholder | TableCellLink - | TableCellChildren ); export function TableCell(props: TableCellProps): JSX.Element { - const { - type, - label, - hasLastBorderNoneClass, - isContentCentered, - textColor: textColorClass = TableCellTextColor.Default, - } = props; + const { type, label, hasLastBorderNoneClass, isContentCentered } = props; + const textColorClass = 'text-neutral-40 dark:text-neutral-60'; const textSizeClass = 'text-body-md'; async function handleCopyClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { @@ -168,19 +143,12 @@ export function TableCell(props: TableCellProps): JSX.Element { } const Cell = () => { - const { noWrap } = props; switch (type) { case TableCellType.Text: const { supportingLabel } = props; return ( <div className="flex flex-row items-baseline gap-1"> - <span - className={cx(textColorClass, textSizeClass, { - 'whitespace-nowrap': noWrap, - })} - > - {label} - </span> + <span className={cx(textColorClass, textSizeClass)}>{label}</span> {supportingLabel && ( <span className="text-body-sm text-neutral-60 dark:text-neutral-40"> {supportingLabel} @@ -208,7 +176,6 @@ export function TableCell(props: TableCellProps): JSX.Element { return <Badge type={badgeType} label={label} />; case TableCellType.AvatarText: const { leadingElement } = props; - return ( <div className={cx('flex items-center gap-x-2.5', textColorClass)}> {leadingElement} @@ -236,16 +203,11 @@ export function TableCell(props: TableCellProps): JSX.Element { href={to} target={isExternal ? '_blank' : '_self'} rel="noopener noreferrer" - className={cx('text-primary-30 dark:text-primary-80', textSizeClass, { - 'whitespace-nowrap': noWrap, - })} + className={cx('text-primary-30 dark:text-primary-80', textSizeClass)} > {label} </a> ); - case TableCellType.Children: - const { children } = props; - return children; default: return null; } diff --git a/apps/ui-kit/src/lib/components/molecules/table-cell/table-cell.enums.ts b/apps/ui-kit/src/lib/components/molecules/table-cell/table-cell.enums.ts index c89759317d8..856dcf3f30e 100644 --- a/apps/ui-kit/src/lib/components/molecules/table-cell/table-cell.enums.ts +++ b/apps/ui-kit/src/lib/components/molecules/table-cell/table-cell.enums.ts @@ -9,10 +9,4 @@ export enum TableCellType { Checkbox = 'checkbox', Placeholder = 'placeholder', Link = 'link', - Children = 'children', -} - -export enum TableCellTextColor { - Default = 'text-neutral-40 dark:text-neutral-60', - Dark = 'text-neutral-10 dark:text-neutral-92', } diff --git a/apps/ui-kit/src/lib/components/molecules/table-header-cell/TableHeaderCell.tsx b/apps/ui-kit/src/lib/components/molecules/table-header-cell/TableHeaderCell.tsx index 84beb087ecb..722e3235b95 100644 --- a/apps/ui-kit/src/lib/components/molecules/table-header-cell/TableHeaderCell.tsx +++ b/apps/ui-kit/src/lib/components/molecules/table-header-cell/TableHeaderCell.tsx @@ -105,13 +105,7 @@ export function TableHeaderCell({ onCheckedChange={onCheckboxChange} /> ) : ( - <span - className={cx({ - 'text-left': !isContentCentered, - })} - > - {label} - </span> + <span>{label}</span> )} {hasSort && sortOrder === TableHeaderCellSortOrder.Asc && ( <SortByUp className="cursor-pointer" onClick={handleSort} /> diff --git a/apps/ui-kit/src/lib/constants/index.ts b/apps/ui-kit/src/lib/constants/index.ts deleted file mode 100644 index 8b691fd8ff1..00000000000 --- a/apps/ui-kit/src/lib/constants/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -export * from './colors.constants'; diff --git a/apps/ui-kit/src/lib/index.ts b/apps/ui-kit/src/lib/index.ts index 4457ead55fc..0e37afefc94 100644 --- a/apps/ui-kit/src/lib/index.ts +++ b/apps/ui-kit/src/lib/index.ts @@ -3,5 +3,4 @@ export * from './tailwind'; export * from './components'; -export * from './constants'; export * from './enums'; diff --git a/apps/ui-kit/src/lib/tailwind/base.preset.ts b/apps/ui-kit/src/lib/tailwind/base.preset.ts index f7ee5e37a5e..9608070bda8 100644 --- a/apps/ui-kit/src/lib/tailwind/base.preset.ts +++ b/apps/ui-kit/src/lib/tailwind/base.preset.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Config } from 'tailwindcss'; -import { IOTA_PRIMITIVES_COLOR_PALETTE, SHADER_COLOR_PALETTE } from '../constants/colors.constants'; +import { IOTA_PRIMITIVES_COLOR_PALETTE, SHADER_COLOR_PALETTE } from './constants/colors.constants'; import { CUSTOM_FONT_SIZES, BORDER_RADIUS, diff --git a/apps/ui-kit/src/lib/constants/colors.constants.ts b/apps/ui-kit/src/lib/tailwind/constants/colors.constants.ts similarity index 100% rename from apps/ui-kit/src/lib/constants/colors.constants.ts rename to apps/ui-kit/src/lib/tailwind/constants/colors.constants.ts diff --git a/apps/ui-kit/src/lib/tailwind/constants/index.ts b/apps/ui-kit/src/lib/tailwind/constants/index.ts index 2adc80fcca0..a481235f30c 100644 --- a/apps/ui-kit/src/lib/tailwind/constants/index.ts +++ b/apps/ui-kit/src/lib/tailwind/constants/index.ts @@ -1,6 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +export * from './colors.constants'; export * from './scaling.constants'; export * from './fontSizes.constants'; export * from './variable-spacing.constants'; diff --git a/apps/ui-kit/src/storybook/stories/atoms/LabelText.stories.tsx b/apps/ui-kit/src/storybook/stories/atoms/LabelText.stories.tsx index d9c99286cdc..f94d22039e5 100644 --- a/apps/ui-kit/src/storybook/stories/atoms/LabelText.stories.tsx +++ b/apps/ui-kit/src/storybook/stories/atoms/LabelText.stories.tsx @@ -22,6 +22,7 @@ export const Default: Story = { text: '12,000.00', label: 'Label', size: LabelTextSize.Medium, + showSupportingLabel: true, supportingLabel: 'IOTA', isCentered: false, }, @@ -39,6 +40,9 @@ export const Default: Story = { supportingLabel: { control: 'text', }, + showSupportingLabel: { + control: 'boolean', + }, text: { control: 'text', }, diff --git a/apps/ui-kit/src/storybook/stories/design-tokens/colors.stories.mdx b/apps/ui-kit/src/storybook/stories/design-tokens/colors.stories.mdx index 94f3f51adaf..18afbd591fe 100644 --- a/apps/ui-kit/src/storybook/stories/design-tokens/colors.stories.mdx +++ b/apps/ui-kit/src/storybook/stories/design-tokens/colors.stories.mdx @@ -2,7 +2,7 @@ import { Meta, ColorPalette, ColorItem } from '@storybook/blocks'; import { IOTA_PRIMITIVES_COLOR_PALETTE, SHADER_COLOR_PALETTE, -} from '../../../lib/constants/colors.constants'; +} from '../../../lib/tailwind/constants/colors.constants'; <Meta title="Design Tokens/Colors" /> diff --git a/apps/ui-kit/src/storybook/stories/molecules/Chip.stories.tsx b/apps/ui-kit/src/storybook/stories/molecules/Chip.stories.tsx index 2c891a3e4c2..a35a45e8e1b 100644 --- a/apps/ui-kit/src/storybook/stories/molecules/Chip.stories.tsx +++ b/apps/ui-kit/src/storybook/stories/molecules/Chip.stories.tsx @@ -42,7 +42,7 @@ export const Default: Story = { export const WithIcon: Story = { args: { label: 'Label', - leadingElement: <PlaceholderReplace />, + icon: <PlaceholderReplace />, }, render: (props) => { return ( diff --git a/apps/wallet-dashboard/package.json b/apps/wallet-dashboard/package.json index 71591c967e1..3e79a7a28fa 100644 --- a/apps/wallet-dashboard/package.json +++ b/apps/wallet-dashboard/package.json @@ -21,7 +21,7 @@ "@iota/iota-sdk": "workspace:*", "@tanstack/react-query": "^5.0.0", "@tanstack/react-virtual": "^3.5.0", - "next": "14.2.10", + "next": "14.2.3", "react": "^18.3.1", "zustand": "^4.4.1" }, diff --git a/apps/wallet/src/shared/utils/url.ts b/apps/wallet/src/shared/utils/url.ts index db39b5e1367..b5366afb2d2 100644 --- a/apps/wallet/src/shared/utils/url.ts +++ b/apps/wallet/src/shared/utils/url.ts @@ -3,7 +3,7 @@ import { getUrlWithDeviceId } from '../analytics/amplitude'; -const IOTA_DAPPS = ['iotafrens.com']; +const IOTA_DAPPS = ['iotafrens.com', 'iotans.io']; export function isValidUrl(url: string | null) { if (!url) { diff --git a/apps/wallet/src/ui/app/components/accounts/AccountItem.tsx b/apps/wallet/src/ui/app/components/accounts/AccountItem.tsx index 84db5e05c69..1ef34a8039c 100644 --- a/apps/wallet/src/ui/app/components/accounts/AccountItem.tsx +++ b/apps/wallet/src/ui/app/components/accounts/AccountItem.tsx @@ -2,6 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { useResolveIotaNSName } from '@iota/core'; import { formatAddress } from '@iota/iota-sdk/utils'; import cn from 'clsx'; import { type ReactNode } from 'react'; @@ -30,7 +31,8 @@ export function AccountItem({ }: AccountItemProps) { const { data: accounts } = useAccounts(); const account = accounts?.find((account) => account.id === accountID); - const accountName = account?.nickname ?? formatAddress(account?.address || ''); + const { data: domainName } = useResolveIotaNSName(account?.address); + const accountName = account?.nickname ?? domainName ?? formatAddress(account?.address || ''); const copyAddress = useCopyToClipboard(account?.address || '', { copySuccessMessage: 'Address copied', }); diff --git a/apps/wallet/src/ui/app/components/accounts/AccountItemApproveConnection.tsx b/apps/wallet/src/ui/app/components/accounts/AccountItemApproveConnection.tsx index 41d7f428e06..7836f40d7f1 100644 --- a/apps/wallet/src/ui/app/components/accounts/AccountItemApproveConnection.tsx +++ b/apps/wallet/src/ui/app/components/accounts/AccountItemApproveConnection.tsx @@ -4,8 +4,9 @@ import { AccountIcon, useUnlockAccount } from '_components'; import { type SerializedUIAccount } from '_src/background/accounts/Account'; -import { formatAddress } from '@iota/iota-sdk/utils'; import { Account } from '@iota/apps-ui-kit'; +import { useResolveIotaNSName } from '@iota/core'; +import { formatAddress } from '@iota/iota-sdk/utils'; interface AccountItemApproveConnectionProps { account: SerializedUIAccount; @@ -16,7 +17,8 @@ export function AccountItemApproveConnection({ account, selected, }: AccountItemApproveConnectionProps) { - const accountName = account?.nickname ?? formatAddress(account?.address || ''); + const { data: domainName } = useResolveIotaNSName(account?.address); + const accountName = account?.nickname ?? domainName ?? formatAddress(account?.address || ''); const { unlockAccount, lockAccount } = useUnlockAccount(); return ( diff --git a/apps/wallet/src/ui/app/components/ledger/LedgerAccountList.tsx b/apps/wallet/src/ui/app/components/ledger/LedgerAccountList.tsx index fc6bc85947d..e1f7431266d 100644 --- a/apps/wallet/src/ui/app/components/ledger/LedgerAccountList.tsx +++ b/apps/wallet/src/ui/app/components/ledger/LedgerAccountList.tsx @@ -14,7 +14,7 @@ import { } from '@iota/apps-ui-kit'; import { type DerivedLedgerAccount } from './useDeriveLedgerAccounts'; import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { useBalance, useFormatCoin } from '@iota/core'; +import { useBalance, useFormatCoin, useResolveIotaNSName } from '@iota/core'; export type SelectableLedgerAccount = DerivedLedgerAccount & { isSelected: boolean; @@ -34,6 +34,7 @@ export function LedgerAccountList({ accounts, onAccountClick, selectAll }: Ledge const rowsData = accounts.map((account) => { const { data: coinBalance } = useBalance(account.address); + const { data: domainName } = useResolveIotaNSName(account.address); const [totalAmount, totalAmountSymbol] = useFormatCoin( coinBalance?.totalBalance ?? 0, IOTA_TYPE_ARG, @@ -41,7 +42,7 @@ export function LedgerAccountList({ accounts, onAccountClick, selectAll }: Ledge return [ { - label: formatAddress(account.address), + label: domainName ?? formatAddress(account.address), }, { label: `${totalAmount} ${totalAmountSymbol}`, diff --git a/apps/wallet/src/ui/app/hooks/useAddressLink.ts b/apps/wallet/src/ui/app/hooks/useAddressLink.ts index 86d0956d087..ccafb941778 100644 --- a/apps/wallet/src/ui/app/hooks/useAddressLink.ts +++ b/apps/wallet/src/ui/app/hooks/useAddressLink.ts @@ -4,9 +4,12 @@ import { useExplorerLink } from '_app/hooks/useExplorerLink'; import { ExplorerLinkType } from '_components'; import { formatAddress } from '@iota/iota-sdk/utils'; +import { useResolveIotaNSName } from '@iota/dapp-kit'; +import { isIotaNSName } from '@iota/core'; export function useAddressLink(inputAddress: string | null) { - const outputAddress = inputAddress || ''; + const { data: domainName } = useResolveIotaNSName(inputAddress); + const outputAddress = domainName ?? (inputAddress || ''); const explorerHref = useExplorerLink({ type: ExplorerLinkType.Address, address: outputAddress || undefined, @@ -15,6 +18,6 @@ export function useAddressLink(inputAddress: string | null) { return { explorerHref: explorerHref || '', addressFull: inputAddress || '', - address: formatAddress(outputAddress), + address: isIotaNSName(outputAddress) ? outputAddress : formatAddress(outputAddress), }; } diff --git a/apps/wallet/src/ui/app/pages/accounts/manage/AccountGroupItem.tsx b/apps/wallet/src/ui/app/pages/accounts/manage/AccountGroupItem.tsx index 58b180292a6..045fe9c47c8 100644 --- a/apps/wallet/src/ui/app/pages/accounts/manage/AccountGroupItem.tsx +++ b/apps/wallet/src/ui/app/pages/accounts/manage/AccountGroupItem.tsx @@ -4,6 +4,7 @@ import { AccountType, type SerializedUIAccount } from '_src/background/accounts/Account'; import { useState } from 'react'; import clsx from 'clsx'; +import { useResolveIotaNSName } from '@iota/core'; import { formatAddress } from '@iota/iota-sdk/utils'; import { ExplorerLinkType, NicknameDialog, useUnlockAccount } from '_components'; import { useNavigate } from 'react-router-dom'; @@ -25,7 +26,8 @@ export function AccountGroupItem({ account, isLast }: AccountGroupItemProps) { const [isDropdownOpen, setDropdownOpen] = useState(false); const [isDialogNicknameOpen, setDialogNicknameOpen] = useState(false); const [isDialogRemoveOpen, setDialogRemoveOpen] = useState(false); - const accountName = account?.nickname ?? formatAddress(account?.address || ''); + const { data: domainName } = useResolveIotaNSName(account?.address); + const accountName = account?.nickname ?? domainName ?? formatAddress(account?.address || ''); const { unlockAccount, lockAccount } = useUnlockAccount(); const navigate = useNavigate(); const allAccounts = useAccounts(); diff --git a/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx b/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx index ee846fab8e0..dee857a9189 100644 --- a/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/nft-transfer/TransferNFTForm.tsx @@ -8,7 +8,13 @@ import { getSignerOperationErrorMessage } from '_src/ui/app/helpers/errorMessage import { useActiveAddress } from '_src/ui/app/hooks'; import { useActiveAccount } from '_src/ui/app/hooks/useActiveAccount'; import { useSigner } from '_src/ui/app/hooks/useSigner'; -import { createNftSendValidationSchema, useGetKioskContents } from '@iota/core'; +import { + createNftSendValidationSchema, + isIotaNSName, + useGetKioskContents, + useIotaNSEnabled, +} from '@iota/core'; +import { useIotaClient } from '@iota/dapp-kit'; import { TransactionBlock } from '@iota/iota-sdk/transactions'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Field, Form, Formik } from 'formik'; @@ -25,7 +31,14 @@ interface TransferNFTFormProps { export function TransferNFTForm({ objectId, objectType }: TransferNFTFormProps) { const activeAddress = useActiveAddress(); - const validationSchema = createNftSendValidationSchema(activeAddress || '', objectId); + const rpc = useIotaClient(); + const iotaNSEnabled = useIotaNSEnabled(); + const validationSchema = createNftSendValidationSchema( + activeAddress || '', + objectId, + rpc, + iotaNSEnabled, + ); const activeAccount = useActiveAccount(); const signer = useSigner(activeAccount); const queryClient = useQueryClient(); @@ -46,6 +59,16 @@ export function TransferNFTForm({ objectId, objectType }: TransferNFTFormProps) return transferKioskItem.mutateAsync({ to }); } + if (iotaNSEnabled && isIotaNSName(to)) { + const address = await rpc.resolveNameServiceAddress({ + name: to, + }); + if (!address) { + throw new Error('IotaNS name not found.'); + } + to = address; + } + const tx = new TransactionBlock(); tx.transferObjects([tx.object(objectId)], to); diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx index 17af878c701..d2497b18338 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/SendTokenForm.tsx @@ -9,8 +9,10 @@ import { useGetAllCoins, CoinFormat, createTokenTransferTransaction, + isIotaNSName, useCoinMetadata, useFormatCoin, + useIotaNSEnabled, parseAmount, } from '@iota/core'; import { useIotaClient } from '@iota/dapp-kit'; @@ -75,6 +77,7 @@ function useGasBudgetEstimation({ }) { const activeAddress = useActiveAddress(); const { values, setFieldValue } = useFormikContext<FormValues>(); + const iotaNSEnabled = useIotaNSEnabled(); const client = useIotaClient(); const { data: gasBudget } = useQuery({ @@ -94,7 +97,16 @@ function useGasBudgetEstimation({ return null; } - const to = values.to; + let to = values.to; + if (iotaNSEnabled && isIotaNSName(values.to)) { + const address = await client.resolveNameServiceAddress({ + name: values.to, + }); + if (!address) { + throw new Error('IotaNS name not found.'); + } + to = address; + } const tx = createTokenTransferTransaction({ to, @@ -153,10 +165,12 @@ export function SendTokenForm({ coinType, CoinFormat.FULL, ); + const iotaNSEnabled = useIotaNSEnabled(); const validationSchemaStepOne = useMemo( - () => createValidationSchemaStepOne(coinBalance, symbol, coinDecimals), - [client, coinBalance, symbol, coinDecimals], + () => + createValidationSchemaStepOne(client, iotaNSEnabled, coinBalance, symbol, coinDecimals), + [client, coinBalance, symbol, coinDecimals, iotaNSEnabled], ); // remove the comma from the token balance @@ -169,6 +183,16 @@ export function SendTokenForm({ .sort((a, b) => Number(b.balance) - Number(a.balance)) .map(({ coinObjectId }) => coinObjectId); + if (iotaNSEnabled && isIotaNSName(to)) { + const address = await client.resolveNameServiceAddress({ + name: to, + }); + if (!address) { + throw new Error('IotaNS name not found.'); + } + to = address; + } + const data = { to, amount, diff --git a/apps/wallet/src/ui/app/pages/home/transfer-coin/validation.ts b/apps/wallet/src/ui/app/pages/home/transfer-coin/validation.ts index dbab4e6e602..bbead006bd0 100644 --- a/apps/wallet/src/ui/app/pages/home/transfer-coin/validation.ts +++ b/apps/wallet/src/ui/app/pages/home/transfer-coin/validation.ts @@ -4,11 +4,16 @@ import { createIotaAddressValidation } from '@iota/core'; import { createTokenValidation } from '_src/shared/validation'; +import { type IotaClient } from '@iota/iota-sdk/client'; import * as Yup from 'yup'; -export function createValidationSchemaStepOne(...args: Parameters<typeof createTokenValidation>) { +export function createValidationSchemaStepOne( + client: IotaClient, + iotaNSEnabled: boolean, + ...args: Parameters<typeof createTokenValidation> +) { return Yup.object({ - to: createIotaAddressValidation(), + to: createIotaAddressValidation(client, iotaNSEnabled), amount: createTokenValidation(...args), }); } diff --git a/apps/wallet/src/ui/app/shared/page-main-layout/PageMainLayout.tsx b/apps/wallet/src/ui/app/shared/page-main-layout/PageMainLayout.tsx index c15632a7009..4f982fb5885 100644 --- a/apps/wallet/src/ui/app/shared/page-main-layout/PageMainLayout.tsx +++ b/apps/wallet/src/ui/app/shared/page-main-layout/PageMainLayout.tsx @@ -15,6 +15,7 @@ import { useActiveAccount } from '../../hooks/useActiveAccount'; import { Link } from 'react-router-dom'; import { formatAddress } from '@iota/iota-sdk/utils'; import { isLedgerAccountSerializedUI } from '_src/background/accounts/LedgerAccount'; +import { useResolveIotaNSName } from '@iota/core'; import { type SerializedUIAccount } from '_src/background/accounts/Account'; export const PageMainLayoutContext = createContext<HTMLDivElement | null>(null); @@ -93,7 +94,8 @@ function LeftContent({ isLedgerAccount: boolean | null; isLocked?: boolean; }) { - const accountName = account?.nickname ?? formatAddress(account?.address || ''); + const { data: domainName } = useResolveIotaNSName(account?.address); + const accountName = account?.nickname ?? domainName ?? formatAddress(account?.address || ''); const backgroundColor = isLocked ? 'bg-neutral-90' : 'bg-primary-30'; return ( <Link diff --git a/crates/iota-json-rpc-types/Cargo.toml b/crates/iota-json-rpc-types/Cargo.toml index 96baec66d2e..f62c781580f 100644 --- a/crates/iota-json-rpc-types/Cargo.toml +++ b/crates/iota-json-rpc-types/Cargo.toml @@ -23,6 +23,8 @@ serde_json.workspace = true serde_with.workspace = true tabled.workspace = true tracing.workspace = true +clap = { version = "4.0", features = ["derive"] } + move-binary-format.workspace = true move-bytecode-utils.workspace = true diff --git a/crates/iota-json-rpc-types/src/iota_transaction.rs b/crates/iota-json-rpc-types/src/iota_transaction.rs index 9a40e65bcb4..9dd842da38a 100644 --- a/crates/iota-json-rpc-types/src/iota_transaction.rs +++ b/crates/iota-json-rpc-types/src/iota_transaction.rs @@ -49,6 +49,7 @@ use tabled::{ builder::Builder as TableBuilder, settings::{style::HorizontalLine, Panel as TablePanel, Style as TableStyle}, }; +use clap::Parser; use crate::{ balance_changes::BalanceChange, iota_transaction::GenericSignature::Signature, @@ -96,20 +97,34 @@ pub type TransactionBlocksPage = Page<IotaTransactionBlockResponse, TransactionD rename = "TransactionBlockResponseOptions", default )] +#[derive(Parser)] pub struct IotaTransactionBlockResponseOptions { /// Whether to show transaction input data. Default to be False + #[clap(long, required = false)] pub show_input: bool, - /// Whether to show bcs-encoded transaction input data + + /// Whether to show BCS-encoded transaction input data + #[clap(long, required = false)] pub show_raw_input: bool, + /// Whether to show transaction effects. Default to be False + #[clap(long, required = false)] pub show_effects: bool, + /// Whether to show transaction events. Default to be False + #[clap(long, required = false)] pub show_events: bool, - /// Whether to show object_changes. Default to be False + + /// Whether to show object changes. Default to be False + #[clap(long, required = false)] pub show_object_changes: bool, - /// Whether to show balance_changes. Default to be False + + /// Whether to show balance changes. Default to be False + #[clap(long, required = false)] pub show_balance_changes: bool, + /// Whether to show raw transaction effects. Default to be False + #[clap(long, required = false)] pub show_raw_effects: bool, } @@ -198,6 +213,25 @@ impl IotaTransactionBlockResponseOptions { pub fn only_digest(&self) -> bool { self == &Self::default() } + + pub fn from_cli(mut self, opts: Vec<String>) -> Self { + if opts.contains(&"input".to_string()) { + self.show_input = true; + } + if opts.contains(&"effects".to_string()) { + self.show_effects = true; + } + if opts.contains(&"events".to_string()) { + self.show_events = true; + } + if opts.contains(&"object_changes".to_string()) { + self.show_object_changes = true; + } + if opts.contains(&"balance_changes".to_string()) { + self.show_balance_changes = true; + } + self + } } #[serde_as] diff --git a/crates/iota-sdk/src/wallet_context.rs b/crates/iota-sdk/src/wallet_context.rs index 35eb62ed9f9..b3a724e30b1 100644 --- a/crates/iota-sdk/src/wallet_context.rs +++ b/crates/iota-sdk/src/wallet_context.rs @@ -299,9 +299,10 @@ impl WalletContext { pub async fn execute_transaction_must_succeed( &self, tx: Transaction, + opts: Vec<String>, ) -> IotaTransactionBlockResponse { tracing::debug!("Executing transaction: {:?}", tx); - let response = self.execute_transaction_may_fail(tx).await.unwrap(); + let response = self.execute_transaction_may_fail(tx, Vec::new()).await.unwrap(); assert!( response.status_ok().unwrap(), "Transaction failed: {:?}", @@ -317,18 +318,14 @@ impl WalletContext { pub async fn execute_transaction_may_fail( &self, tx: Transaction, + opts: Vec<String>, ) -> anyhow::Result<IotaTransactionBlockResponse> { let client = self.get_client().await?; Ok(client .quorum_driver_api() .execute_transaction_block( tx, - IotaTransactionBlockResponseOptions::new() - .with_effects() - .with_input() - .with_events() - .with_object_changes() - .with_balance_changes(), + IotaTransactionBlockResponseOptions::new().from_cli(opts), Some(iota_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution), ) .await?) diff --git a/crates/iota-test-transaction-builder/src/lib.rs b/crates/iota-test-transaction-builder/src/lib.rs index 45efb327b8a..a44a8bdbdb3 100644 --- a/crates/iota-test-transaction-builder/src/lib.rs +++ b/crates/iota-test-transaction-builder/src/lib.rs @@ -528,7 +528,7 @@ pub async fn publish_package(context: &WalletContext, path: PathBuf) -> ObjectRe .publish(path) .build(), ); - let resp = context.execute_transaction_must_succeed(txn).await; + let resp = context.execute_transaction_must_succeed(txn, Vec::new()).await; get_new_package_obj_from_response(&resp).unwrap() } @@ -542,7 +542,7 @@ pub async fn publish_basics_package(context: &WalletContext) -> ObjectRef { .publish_examples("basics") .build(), ); - let resp = context.execute_transaction_must_succeed(txn).await; + let resp = context.execute_transaction_must_succeed(txn, Vec::new()).await; get_new_package_obj_from_response(&resp).unwrap() } @@ -560,7 +560,7 @@ pub async fn publish_basics_package_and_make_counter( .build(), ); let resp = context - .execute_transaction_must_succeed(counter_creation_txn) + .execute_transaction_must_succeed(counter_creation_txn, Vec::new()) .await; let counter_ref = resp .effects @@ -599,7 +599,7 @@ pub async fn increment_counter( .call_counter_increment(package_id, counter_id, initial_shared_version) .build(), ); - context.execute_transaction_must_succeed(txn).await + context.execute_transaction_must_succeed(txn, Vec::new()).await } /// Executes a transaction to publish the `nfts` package and returns the package @@ -615,7 +615,7 @@ pub async fn publish_nfts_package( .publish_examples("nfts") .build(), ); - let resp = context.execute_transaction_must_succeed(txn).await; + let resp = context.execute_transaction_must_succeed(txn, Vec::new()).await; let package_id = get_new_package_obj_from_response(&resp).unwrap().0; (package_id, gas_id, resp.digest) } @@ -635,7 +635,7 @@ pub async fn create_devnet_nft( .call_nft_create(package_id) .build(), ); - let resp = context.execute_transaction_must_succeed(txn).await; + let resp = context.execute_transaction_must_succeed(txn, Vec::new()).await; let object_id = resp .effects @@ -668,5 +668,5 @@ pub async fn delete_devnet_nft( .call_nft_delete(package_id, nft_to_delete) .build(), ); - context.execute_transaction_must_succeed(txn).await + context.execute_transaction_must_succeed(txn, Vec::new()).await } diff --git a/crates/iota/src/client_commands.rs b/crates/iota/src/client_commands.rs index 0a461867f68..912e30a4592 100644 --- a/crates/iota/src/client_commands.rs +++ b/crates/iota/src/client_commands.rs @@ -85,7 +85,7 @@ mod profiler_tests; #[macro_export] macro_rules! serialize_or_execute { - ($tx_data:expr, $serialize_unsigned:expr, $serialize_signed:expr, $context:expr, $result_variant:ident) => {{ + ($tx_data:expr, $serialize_unsigned:expr, $serialize_signed:expr, $context:expr, $result_variant:ident, $opts:expr) => {{ assert!( !$serialize_unsigned || !$serialize_signed, "Cannot specify both --serialize-unsigned-transaction and --serialize-signed-transaction" @@ -107,7 +107,7 @@ macro_rules! serialize_or_execute { IotaClientCommandResult::SerializedSignedTransaction(sender_signed_data) } else { let transaction = Transaction::new(sender_signed_data); - let response = $context.execute_transaction_may_fail(transaction).await?; + let response = $context.execute_transaction_may_fail(transaction, $opts).await?; let effects = response.effects.as_ref().ok_or_else(|| { anyhow!("Effects from IotaTransactionBlockResult should not be empty") })?; @@ -178,7 +178,6 @@ pub enum IotaClientCommands { #[clap(long, num_args(1..))] args: Vec<IotaJsonValue>, /// ID of the gas object for gas payment, in 20 bytes Hex string - #[clap(long)] /// If not provided, a gas object with at least gas_budget value will be /// selected #[clap(long)] @@ -208,6 +207,12 @@ pub enum IotaClientCommands { /// <SIGNED_TX_BYTES>`. #[clap(long, required = false)] serialize_signed_transaction: bool, + + /// Select which fields of the response to display. + /// If not provided, all fields are displayed. + /// The fields are: input, effects, events, object_changes, balance_changes. + #[clap(long, required = false)] + emit: Vec<String>, }, /// Query the chain identifier from the rpc endpoint. @@ -1099,7 +1104,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Upgrade + Upgrade, + Vec::new() ) } IotaClientCommands::Publish { @@ -1155,7 +1161,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Publish + Publish, + Vec::new() ) } @@ -1266,6 +1273,7 @@ impl IotaClientCommands { args, serialize_unsigned_transaction, serialize_signed_transaction, + emit } => { let tx_data = construct_move_call_transaction( package, &module, &function, type_args, gas, gas_budget, gas_price, args, @@ -1277,7 +1285,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Call + Call, + emit ) } @@ -1301,7 +1310,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Transfer + Transfer, + Vec::new() ) } @@ -1325,7 +1335,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - TransferIota + TransferIota, + Vec::new() ) } @@ -1370,7 +1381,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - Pay + Pay, + Vec::new() ) } @@ -1414,7 +1426,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - PayIota + PayIota, + Vec::new() ) } @@ -1442,7 +1455,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - PayAllIota + PayAllIota, + Vec::new() ) } @@ -1582,7 +1596,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - SplitCoin + SplitCoin, + Vec::new() ) } IotaClientCommands::MergeCoin { @@ -1604,7 +1619,8 @@ impl IotaClientCommands { serialize_unsigned_transaction, serialize_signed_transaction, context, - MergeCoin + MergeCoin, + Vec::new() ) } IotaClientCommands::Switch { address, env } => { @@ -1660,7 +1676,7 @@ impl IotaClientCommands { } let transaction = Transaction::from_generic_sig_data(data, sigs); - let response = context.execute_transaction_may_fail(transaction).await?; + let response = context.execute_transaction_may_fail(transaction, Vec::new()).await?; IotaClientCommandResult::ExecuteSignedTx(response) } IotaClientCommands::ExecuteCombinedSignedTx { signed_tx_bytes } => { @@ -1671,7 +1687,7 @@ impl IotaClientCommands { .map_err(|_| anyhow!("Invalid Base64 encoding"))? ).map_err(|_| anyhow!("Failed to parse SenderSignedData bytes, check if it matches the output of iota client commands with --serialize-signed-transaction"))?; let transaction = Envelope::<SenderSignedData, EmptySignInfo>::new(data); - let response = context.execute_transaction_may_fail(transaction).await?; + let response = context.execute_transaction_may_fail(transaction, Vec::new()).await?; IotaClientCommandResult::ExecuteSignedTx(response) } IotaClientCommands::NewEnv { alias, rpc, ws } => { diff --git a/crates/iota/src/client_ptb/ptb.rs b/crates/iota/src/client_ptb/ptb.rs index f039a5e6b54..00aaa9232ab 100644 --- a/crates/iota/src/client_ptb/ptb.rs +++ b/crates/iota/src/client_ptb/ptb.rs @@ -168,12 +168,12 @@ impl PTB { ); if program_metadata.serialize_unsigned_set { - serialize_or_execute!(tx_data, true, false, context, PTB).print(true); + serialize_or_execute!(tx_data, true, false, context, PTB, Vec::new()).print(true); return Ok(()); } if program_metadata.serialize_signed_set { - serialize_or_execute!(tx_data, false, true, context, PTB).print(true); + serialize_or_execute!(tx_data, false, true, context, PTB, Vec::new()).print(true); return Ok(()); } diff --git a/crates/iota/tests/cli_tests.rs b/crates/iota/tests/cli_tests.rs index c24ab755557..58ea7b3914d 100644 --- a/crates/iota/tests/cli_tests.rs +++ b/crates/iota/tests/cli_tests.rs @@ -313,6 +313,7 @@ async fn test_ptb_publish_and_complex_arg_resolution() -> Result<(), anyhow::Err args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -660,6 +661,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: None, serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -700,6 +702,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: None, serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await; @@ -727,6 +730,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: None, serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await; @@ -751,6 +755,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: Some(1), serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await; @@ -784,6 +789,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: None, serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -805,6 +811,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { gas_price: Some(12345), serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -962,6 +969,7 @@ async fn test_delete_shared_object() -> Result<(), anyhow::Error> { args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -985,6 +993,7 @@ async fn test_delete_shared_object() -> Result<(), anyhow::Error> { args: vec![IotaJsonValue::from_str(&shared_id.to_string()).unwrap()], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1071,6 +1080,7 @@ async fn test_receive_argument() -> Result<(), anyhow::Error> { args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1113,6 +1123,7 @@ async fn test_receive_argument() -> Result<(), anyhow::Error> { ], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1199,6 +1210,7 @@ async fn test_receive_argument_by_immut_ref() -> Result<(), anyhow::Error> { args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1241,6 +1253,7 @@ async fn test_receive_argument_by_immut_ref() -> Result<(), anyhow::Error> { ], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1327,6 +1340,7 @@ async fn test_receive_argument_by_mut_ref() -> Result<(), anyhow::Error> { args: vec![], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; @@ -1369,6 +1383,7 @@ async fn test_receive_argument_by_mut_ref() -> Result<(), anyhow::Error> { ], serialize_unsigned_transaction: false, serialize_signed_transaction: false, + emit: Vec::new(), } .execute(context) .await?; diff --git a/crates/test-cluster/src/lib.rs b/crates/test-cluster/src/lib.rs index 10905559749..10769019214 100644 --- a/crates/test-cluster/src/lib.rs +++ b/crates/test-cluster/src/lib.rs @@ -562,7 +562,7 @@ impl TestCluster { /// ExecutionStatus::Success. This function is recommended for /// transaction execution since it most resembles the production path. pub async fn execute_transaction(&self, tx: Transaction) -> IotaTransactionBlockResponse { - self.wallet.execute_transaction_must_succeed(tx).await + self.wallet.execute_transaction_must_succeed(tx, Vec::new()).await } /// Different from `execute_transaction` which returns RPC effects types, @@ -582,7 +582,7 @@ impl TestCluster { let results = self .submit_transaction_to_validators(tx.clone(), &self.get_validator_pubkeys()) .await?; - self.wallet.execute_transaction_may_fail(tx).await.unwrap(); + self.wallet.execute_transaction_may_fail(tx, Vec::new()).await.unwrap(); Ok(results) } @@ -677,7 +677,7 @@ impl TestCluster { .transfer_iota(amount, funding_address) .build(), ); - context.execute_transaction_must_succeed(tx).await; + context.execute_transaction_must_succeed(tx, Vec::new()).await; context .get_one_gas_object_owned_by_address(funding_address) diff --git a/docker/fullnode/README.md b/docker/fullnode/README.md index eea6c38606f..36f03c63b2a 100644 --- a/docker/fullnode/README.md +++ b/docker/fullnode/README.md @@ -10,25 +10,21 @@ Follow the steps in this Readme to install and configure a Iota Full node for te - [Install Docker](https://docs.docker.com/get-docker/) - [Install Docker Compose](https://docs.docker.com/compose/install/) -- Download the Full node [docker-compose.yaml](https://github.com/iotaledger/iota/blob/develop/docker/fullnode/docker-compose.yaml) file. +- Download the Full node [docker-compose.yaml](https://github.com/iotaledger/iota/blob/main/docker/fullnode/docker-compose.yaml) file. ## Configure Iota Full node -Download the latest version of the Iota Full node configuration file [fullnode-template.yaml](https://github.com/iotaledger/iota/raw/develop/crates/iota-config/data/fullnode-template.yaml). Use the following command to download the file: +Download the latest version of the Iota Full node configuration file [fullnode-template.yaml](https://github.com/iotaledger/iota/raw/main/crates/iota-config/data/fullnode-template.yaml). Use the following command to download the file: ```shell -wget https://github.com/iotaledger/iota/raw/develop/crates/iota-config/data/fullnode-template.yaml +wget https://github.com/iotaledger/iota/raw/main/crates/iota-config/data/fullnode-template.yaml ``` -To ensure you add the appropriate peers for your full node, please refer to the instructions provided at the following link: -https://docs.iota.org/operator/iota-full-node#set-up-from-source and proceed with step 4. - ### Download the Iota genesis blob -The genesis blob contains the information that defined the Iota network configuration. Before you can start the Full node, you need to download the most recent file to ensure compatibility with the version of Iota you use. +The genesis blob contains the information that defined the Iota network configuration. Before you can start the Full node, you need to download the most recent file to ensure compatibility with the version of Iota you use. Use the following command to download the [genesis.blob](https://github.com/iotaledger/iota-genesis/raw/main/devnet/genesis.blob) from the `devnet` branch of the Iota repository: -To download the appropriate genesis.blob file, please refer to the instructions provided at the following link: -https://docs.iota.org/operator/iota-full-node#set-up-from-source. and proceed with step 3. +`wget https://github.com/iotaledger/iota-genesis/raw/main/devnet/genesis.blob` ## Start your Iota Full node @@ -46,7 +42,7 @@ After the Full node starts you can test the JSON-RPC interfaces. ## View activity on your local Full node with Iota Explorer -Iota Explorer supports connecting to a local network. To view activity on your local Full node, open the URL: [https://explorer.iota.org/?network=local](https://explorer.iota.org/?network=local). +Iota Explorer supports connecting to a local network. To view activity on your local Full node, open the URL: [https://explorer.iota.io/?network=local](https://explorer.iota.io/?network=local). You can also change the network that Iota Explorer connects to by select it in the Iota Explorer interface. diff --git a/docs/content/references/execution-architecture/adapter.mdx b/docs/content/about-iota/execution-architecture/adapter.mdx similarity index 100% rename from docs/content/references/execution-architecture/adapter.mdx rename to docs/content/about-iota/execution-architecture/adapter.mdx diff --git a/docs/content/references/execution-architecture/execution-layer.mdx b/docs/content/about-iota/execution-architecture/execution-layer.mdx similarity index 87% rename from docs/content/references/execution-architecture/execution-layer.mdx rename to docs/content/about-iota/execution-architecture/execution-layer.mdx index c0f04f857b3..7d5a5782bfd 100644 --- a/docs/content/references/execution-architecture/execution-layer.mdx +++ b/docs/content/about-iota/execution-architecture/execution-layer.mdx @@ -9,4 +9,4 @@ It includes: - The [IOTA Execution Crate](iota-execution.mdx). - The [Adapter](adapter.mdx) and the [MoveVM](adapter.mdx#move-vm). - The [Native Functions & Object Runtime](natives.mdx). -- The [IOTA Framework](../framework.mdx). \ No newline at end of file +- The [IOTA Framework](../../references/framework.mdx). \ No newline at end of file diff --git a/docs/content/references/execution-architecture/iota-execution.mdx b/docs/content/about-iota/execution-architecture/iota-execution.mdx similarity index 100% rename from docs/content/references/execution-architecture/iota-execution.mdx rename to docs/content/about-iota/execution-architecture/iota-execution.mdx diff --git a/docs/content/references/execution-architecture/natives.mdx b/docs/content/about-iota/execution-architecture/natives.mdx similarity index 100% rename from docs/content/references/execution-architecture/natives.mdx rename to docs/content/about-iota/execution-architecture/natives.mdx diff --git a/docs/content/about-iota/iota-architecture/iota-architecture.mdx b/docs/content/about-iota/iota-architecture/iota-architecture.mdx index 540a16fa536..6fffb07ff89 100644 --- a/docs/content/about-iota/iota-architecture/iota-architecture.mdx +++ b/docs/content/about-iota/iota-architecture/iota-architecture.mdx @@ -34,7 +34,7 @@ The following diagram describes the architectural structure for IOTA's solution. The core components are: -- [Execution Layer](../../references/execution-architecture/execution-layer.mdx) +- [Execution Layer](../execution-architecture/execution-layer.mdx) - [IOTA Node](../../operator/iota-full-node.mdx) - [IOTA RPC](../../references/iota-api) - [IOTA CLI](../../references/cli) diff --git a/docs/content/developer/cryptography/on-chain/ecvrf.mdx b/docs/content/developer/cryptography/on-chain/ecvrf.mdx index 308dedaf339..ac1c28bf760 100644 --- a/docs/content/developer/cryptography/on-chain/ecvrf.mdx +++ b/docs/content/developer/cryptography/on-chain/ecvrf.mdx @@ -52,7 +52,7 @@ module math::ecvrf_test { use iota::event; /// Event on whether the output is verified - public struct VerifiedEvent has copy, drop { + struct VerifiedEvent has copy, drop { is_verified: bool, } diff --git a/docs/content/developer/cryptography/on-chain/hashing.mdx b/docs/content/developer/cryptography/on-chain/hashing.mdx index ed63b7d95eb..3828658c8be 100644 --- a/docs/content/developer/cryptography/on-chain/hashing.mdx +++ b/docs/content/developer/cryptography/on-chain/hashing.mdx @@ -22,9 +22,10 @@ module test::hashing_std { use iota::object::{Self, UID}; use iota::tx_context::TxContext; use iota::transfer; + use std::vector; /// Object that holds the output hash value. - public struct Output has key, store { + struct Output has key, store { id: UID, value: vector<u8> } @@ -48,9 +49,10 @@ module test::hashing_iota { use iota::object::{Self, UID}; use iota::tx_context::TxContext; use iota::transfer; + use std::vector; /// Object that holds the output hash value. - public struct Output has key, store { + struct Output has key, store { id: UID, value: vector<u8> } diff --git a/docs/content/developer/developer.mdx b/docs/content/developer/developer.mdx index aa7aca661d6..10774af912b 100644 --- a/docs/content/developer/developer.mdx +++ b/docs/content/developer/developer.mdx @@ -5,23 +5,23 @@ description: Guides and documents for developers on the IOTA network. Whether yo The developer section is meant to introduce you to the Move programming language and its implementation on the IOTA network through examples, tasks, and conceptual content. This contains everything a developer needs to get started developing on top of the IOTA network. -## Getting Started +## Getting started -If you are completely new to Move, you should start with the aptly named Getting Started section. Topics in that section introduce you to the IOTA monorepo, guide you through installing IOTA binaries, and introduce you to some key core concepts of blockchain technology, particularly how they relate to IOTA. This sections contains everything you need to get your first dApp built and deployed. +If you are completely new to Move, you should start with the aptly named Getting Started section. Topics in that section introduce you to the IOTA monorepo, guide you through installing IOTA binaries, and introduce you to some key core concepts of blockchain technology, particularly how they relate to IOTA. Go to [Getting Started](getting-started/getting-started.mdx). -## IOTA 101 +## Your first dApp -The IOTA 101 section introduces the basics of IOTA that help you create smart contracts. These topics assume you are familiar with Move and the IOTA blockchain. +If you prefer to jump right in to coding (after installing IOTA, of course), then the Your First dApp is the place for you. These topics show you how to work with Move packages and get them published on-chain. -Go to [IOTA 101](iota-101.mdx). +Go to [Your First IOTA dApp](getting-started/create-a-package.mdx). -## Standards +## IOTA 101 -Utilizing standards while developing applications is very important for composability. In this section you can find out all about the standards within the IOTA Move ecosystem for dealing with tokens, NFT like objects, and wallets. +The IOTA 101 section introduces the basics of IOTA that help you create smart contracts. These topics assume you are familiar with Move and the IOTA blockchain. -Go to the [Standards](/developer/standards). +Go to [IOTA 101](iota-101.mdx). ## IOTA compared to EVM @@ -35,25 +35,19 @@ The Cryptography section demonstrates how to secure your smart contracts with cr Go to [Cryptography](cryptography.mdx). -## Advanced Topics - -The Advanced Topics section includes guides for advanced solutions (like asset tokenization). These topics assume you are familiar with Move and the IOTA blockchain. - -Go to the [Advanced Topics](advanced.mdx). - -## Migrating from IOTA/Shimmer Stardust +## Standards -IOTA Stardust becomes IOTA Rebased, and with it a lot of changes. Most assets are just available on the IOTA Rebased chain once it's available without any manual work but for integrations and in some situations you might need to migrate. This section covers the details of the migration and how you can prepare your integration to be IOTA Rebased compatible. +Utilizing standards while developing applications is very important for composability. In this section you can find out all about the standards within the IOTA Move ecosystem for dealing with tokens, NFT like objects, and wallets. -Go to [Migrating from IOTA/Shimmer Stardust](stardust/stardust-migration.mdx). +Go to the [Standards](/developer/standards). -## Exchange Integration +## Advanced Topics -This section contains the technical details needed to integrate IOTA on a exchange. +The Advanced Topics section includes guides for advanced solutions (like asset tokenization). These topics assume you are familiar with Move and the IOTA blockchain. -Go to [Exchange integration](exchange-integration/exchange-integration.mdx). +Go to the [Advanced Topics](advanced.mdx). -## Developer Cheat Sheet +## Developer cheat sheet The cheat sheet is a compact overview of solutions for common problems and contains information on how to avoid common pitfalls for IOTA Move developers. This page contains important information that might get lost in the quantity of content available. Use this often-updated page to see around corners when starting a Move project or to refresh your memory on important concepts to be mindful of. @@ -65,9 +59,14 @@ Everything you need to know about our Layer 2 EVM Support, including working wit Go to [EVM Smart Contracts](iota-evm/introduction.mdx). -## Decentralized Identity +## Migrating from IOTA/Shimmer Stardust + +IOTA Stardust becomes IOTA Rebased, and with it a lot of changes. Most assets are just available on the IOTA Rebased chain once it's available without any manual work but for integrations and in some situations you might need to migrate. This section covers the details of the migration and how you can prepare your integration to be IOTA Rebased compatible. + +Go to [Migrating from IOTA/Shimmer Stardust](stardust/stardust-migration.mdx). -IOTA also offers a Decentralized Identity framework which can be utilized to provide and use digital decentralized identities for People, Organizations, Things and Objects. +## Exchange integration -Go to the [IOTA Identity Framework](iota-identity/welcome.mdx) section. +This section contains the technical details needed to integrate IOTA on a exchange. +Go to [Exchange integration](exchange-integration/exchange-integration.mdx). diff --git a/docs/content/developer/evm-to-move/creating-token.mdx b/docs/content/developer/evm-to-move/creating-token.mdx index f8597fd1903..f3c8dbf6797 100644 --- a/docs/content/developer/evm-to-move/creating-token.mdx +++ b/docs/content/developer/evm-to-move/creating-token.mdx @@ -36,12 +36,13 @@ The initial creation of `Coin` takes place by minting these coins. This is usual ```move title="examples/Exampletoken.move" module examples::exampletoken { + use std::option; use iota::coin; use iota::transfer; use iota::tx_context::{Self, TxContext}; /// One-Time-Witness of kind: `Coin<package_object::exampletoken::EXAMPLETOKEN>` - public struct EXAMPLETOKEN has drop {} + struct EXAMPLETOKEN has drop {} fun init(witness: EXAMPLETOKEN, ctx: &mut TxContext) { let (treasurycap, metadata) = coin::create_currency( @@ -70,7 +71,7 @@ A `module` is defined (`exampletoken`) as part of the `examples` package (module After the aliases, we see an empty struct defined: ```move -public struct EXAMPLETOKEN has drop {} +struct EXAMPLETOKEN has drop {} ``` This is the definition of the so-called [One-Time-Witness](../iota-101/move-overview/one-time-witness.mdx). Together with the `init` function definition, this ensures that this `Coin` will only be created once and never more on this chain. diff --git a/docs/content/developer/getting-started/connect.mdx b/docs/content/developer/getting-started/connect.mdx index ee23e1c8e02..d897fdc7ced 100644 --- a/docs/content/developer/getting-started/connect.mdx +++ b/docs/content/developer/getting-started/connect.mdx @@ -10,13 +10,11 @@ import AlphaNet from "../../_snippets/alphanet.mdx"; ## IOTA CLI -IOTA provides the [IOTA command line interface (CLI)](/references/cli/client.mdx) to interact with [IOTA networks](#iota-networks), it does the following and more: +IOTA provides [IOTA command line interface (CLI)](/references/cli/client.mdx) to interact with [IOTA networks](#iota-networks): - Create and manage your private keys +- Create example NFTs - Call and publish Move modules -- Compile and test Move modules -- Create and execute programmable transaction blocks ([PTBs](/references/cli/ptb.mdx)) -- Interact with testnet/devnet faucets You can use the [CLI](iota-environment.mdx#iota-cli) or [SDKs](iota-environment.mdx#iota-sdks) to send transactions and read requests from using the [JSON-RPC](../../references/iota-api/json-rpc-format.mdx) and the following @@ -92,12 +90,6 @@ Select key scheme to generate keypair (0 for ed25519, 1 for secp256k1, 2: for se Finally, you will be prompted to select the key scheme you want to use. After this, you will receive a message that states the selected key-scheme, the generated address and your secret recovery phrase. -:::tip Signature scheme selection - -If you are unsure which scheme to use just go with the default ed25519 scheme (option 0). - -::: - ```bash Generated new keypair for address with scheme "ed25519" [0xb9c83a8b40d3263c9ba40d551514fbac1f8c12e98a4005a0dac072d3549c2442] Secret Recovery Phrase : [hat become demise beyond history wood stage add nice list jaguar legend] diff --git a/docs/content/developer/getting-started/create-a-package.mdx b/docs/content/developer/getting-started/create-a-package.mdx index 653e2ce44af..7c9b61b791b 100644 --- a/docs/content/developer/getting-started/create-a-package.mdx +++ b/docs/content/developer/getting-started/create-a-package.mdx @@ -31,7 +31,7 @@ In `.toml` files, use the hash mark (`#`) to denote a comment. ```toml title="my_first_package/Move.toml" [package] name = "my_first_package" -edition = "2024.beta" # edition +edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move # license = "" # e.g., "MIT", "GPL", "Apache 2.0" # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] @@ -43,7 +43,7 @@ Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-f # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } # For local dependencies use `local = path`. Path is relative to the package root -# Iota = { local = "../path/to/iota/crates/iota-framework/packages/iota-framework" } +# Local = { local = "../path/to" } # To resolve a version conflict and force a specific version for dependency # override use `override = true` @@ -67,12 +67,6 @@ my_first_package = "0x0" # alice = "0xB0B" ``` -:::tip Using a local version of IOTA - -For local testnet development and testing it is recommended to use the local dependency of the `Iota` framework for faster and more reliable builds. See the commented line in the example above as an example pointing towards your local checkout of the `iota` repository. - -::: - ### Package The `[package]` section describes the package. By default, the `iota move new` command populates only the `name` value @@ -89,13 +83,13 @@ repository URL or a path to the local directory. ```rust # git repository -Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "framework/testnet" } +iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "framework/testnet" } # local directory MyPackage = { local = "../my-package" } ``` -Packages also import addresses from other packages. For example, the `Iota` dependency adds the `std` and `iota` addresses +Packages also import addresses from other packages. For example, the IOTA dependency adds the `std` and `iota` addresses to the project. These addresses can be used in the code as aliases for the addresses. ### Resolving Version Conflicts with `override` @@ -106,7 +100,7 @@ the `[dependencies]` section will be used instead of the one specified in the de ```rust [dependencies] -Iota = { override = true, git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "framework/testnet" } +iota = { override = true, git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "framework/testnet" } ``` ### Dev-dependencies diff --git a/docs/content/developer/getting-started/get-address.mdx b/docs/content/developer/getting-started/get-address.mdx index 8aa74e7d322..b88e4b57fe2 100644 --- a/docs/content/developer/getting-started/get-address.mdx +++ b/docs/content/developer/getting-started/get-address.mdx @@ -67,7 +67,7 @@ IOTA provides multiple ways to obtain an IOTA address. The following are the two :::note Supported Address Types -IOTA supports multiple signature schemes for account generation. The currently supported curves +IOTA supports multiple cryptographic algorithms for account generation. The two supported curves are `ed25519`, `secp256k1`, and `secp256r1`. ::: diff --git a/docs/content/developer/getting-started/iota-install.mdx b/docs/content/developer/getting-started/iota-install.mdx index 2c5d905d88e..d069f0d9591 100644 --- a/docs/content/developer/getting-started/iota-install.mdx +++ b/docs/content/developer/getting-started/iota-install.mdx @@ -24,9 +24,9 @@ always [build the binaries yourself from source](#install-from-source). IOTA supports the following operating systems: -- Linux (Ubuntu/Debian recommended) -- macOS -- Microsoft Windows (10+) +- Linux - Ubuntu version 20.04 (Bionic Beaver) +- macOS - macOS Monterey +- Microsoft Windows - Windows 10 and 11 {/* TODO: Re-enable and check if we have packages published */} {/* @@ -76,7 +76,7 @@ install the necessary binaries. You should start with the main IOTA binary. 2. iota-faucet-`<OS>`-`<ARCHITECTURE>`: Local faucet to mint coins on a local network. 3. iota-indexer-`<OS>`-`<ARCHITECTURE>`: An indexer for a local IOTA network. 4. iota-node-`<OS>`-`<ARCHITECTURE>`: Run a local node. -5. iota-test-validator-`<OS>`-`<ARCHITECTURE>`: Run test validators on a local network for development, including indexer and faucet. +5. iota-test-validator-`<OS>`-`<ARCHITECTURE>`: Run test validators on a local network for development. 6. iota-tool-`<OS>`-`<ARCHITECTURE>`: Provides utilities for IOTA. :::note diff --git a/docs/content/developer/getting-started/local-network.mdx b/docs/content/developer/getting-started/local-network.mdx index df113e650b9..6c74dae57d9 100644 --- a/docs/content/developer/getting-started/local-network.mdx +++ b/docs/content/developer/getting-started/local-network.mdx @@ -5,12 +5,12 @@ tags: [ how-to, install, setup, cli, typescript, sdk, testing ] # Local Development You can create a local IOTA network to develop and test your dApps with the latest changes in the IOTA repository. You -can set it up using the `iota-test-validator` binary, which will start a local IOTA network consisting of 4 IOTA validators with indexer and a +can set it up using the `iota-test-validator` binary, which will start a local IOTA Full node, an IOTA validator, and a test token faucet. ## Prerequisites -You should [install the IOTA CLI tool](iota-install.mdx) to interact with the local node. +You should [install IOTA](iota-install.mdx) to interact with the local node. ## Start a Local Network @@ -165,7 +165,7 @@ The response resembles the following, but with different IDs: ### Prerequisites -* [pnpm](https://pnpm.io/installation) +* [pnpm](https://pnpm.io/installation). ### Install Dependencies diff --git a/docs/content/operator/iota-full-node.mdx b/docs/content/operator/iota-full-node.mdx index ecafd65f858..7502a475929 100644 --- a/docs/content/operator/iota-full-node.mdx +++ b/docs/content/operator/iota-full-node.mdx @@ -13,13 +13,13 @@ IOTA Full nodes validate blockchain activities, including transactions, checkpoi This role enables validators to focus on servicing and processing transactions. When a validator commits a new set of transactions (or a block of transactions), the validator pushes that block to all connected Full nodes that then service the queries from clients. -## Features +## Features IOTA Full nodes: - Track and verify the state of the blockchain, independently and locally. - Serve read requests from clients. -## State synchronization +## State synchronization IOTA Full nodes sync with validators to receive new transactions on the network. A transaction requires a few round trips to 2f+1 validators to form a transaction certificate (TxCert). @@ -32,7 +32,7 @@ This synchronization process includes: This synchronization process requires listening to at a minimum 2f+1 validators to ensure that a Full node has properly processed all new transactions. IOTA will improve the synchronization process with the introduction of checkpoints and the ability to synchronize with other Full nodes. -## Architecture +## Architecture An IOTA Full node is essentially a read-only view of the network state. Unlike validator nodes, Full nodes cannot sign transactions, although they can validate the integrity of the chain by re-executing transactions that a quorum of validators previously committed. @@ -40,11 +40,11 @@ Today, an IOTA Full node maintains the full history of the chain. Validator nodes store only the latest transactions on the frontier of the object graph (for example, transactions with >0 unspent output objects). -## Full node setup +## Full node setup Follow the instructions here to run your own IOTA Full node. -### Hardware requirements +### Hardware requirements Suggested minimum hardware to run an IOTA Full node: @@ -52,7 +52,7 @@ Suggested minimum hardware to run an IOTA Full node: - RAM: 128 GB - Storage (SSD): 4 TB NVMe drive -### Software requirements +### Software requirements IOTA recommends running IOTA Full nodes on Linux. IOTA supports the Ubuntu and Debian distributions. You can also run an IOTA Full node on macOS. @@ -78,15 +78,15 @@ clang \ cmake ``` -## Configure a Full node +## Configure a Full node You can configure an IOTA Full node either using Docker or by building from source. -### Using Docker Compose +### Using Docker Compose Follow the instructions in the [Full node Docker Readme](https://github.com/iotaledger/iota/tree/develop/docker/fullnode#readme) to run an IOTA Full node using Docker, including [resetting the environment](https://github.com/iotaledger/iota/tree/develop/docker/fullnode#reset-the-environment). -### Setting up a local IOTA repository +### Setting up a local IOTA repository You must get the latest source files from the IOTA GitHub repository. 1. Set up your fork of the IOTA repository: @@ -109,23 +109,23 @@ Open a terminal or console to the `iota` directory you downloaded in the previou 1. Make a copy of the [Full node YAML template](https://github.com/iotaledger/iota/blob/develop/crates/iota-config/data/fullnode-template.yaml): `cp crates/iota-config/data/fullnode-template.yaml fullnode.yaml` 1. Download the genesis blob for the network to use: - - [Alphanet genesis blob](https://dbfiles.iota-rebased-alphanet.iota.cafe/genesis.blob): - `curl -fLJO https://dbfiles.iota-rebased-alphanet.iota.cafe/genesis.blob` + - [Devnet genesis blob](https://github.com/iotaledger/iota/TODO): + `curl -fLJO TODO` - [Testnet genesis blob](https://github.com/iotaledger/iota/TODO): `curl -fLJO TODO` - [Mainnet genesis blob](https://github.com/iotaledger/iota/TODO): `curl -fLJO TODO` -1. For Alphanet or Testnet: Edit the `fullnode.yaml` file to include peer nodes for state synchronization. Append the following to the end of the current configuration: +1. For Testnet or Mainnet: Edit the `fullnode.yaml` file to include peer nodes for state synchronization. Append the following to the end of the current configuration: <Tabs groupId="network"> - <TabItem label="Alphanet" value="alphanet"> + <TabItem label="Mainnet" value="mainnet"> ```yaml p2p-config: seed-peers: - - address: /dns/access-0.r.iota-rebased-alphanet.iota.cafe/udp/8084 - peer-id: 10cbea76ea5ec3f7ee827f0c11f612f0059949127876dd964b09304bf8808d18 + - address: TODO # Example: /dns/mel-00.mainnet.iota.io/udp/8084 + peer-id: TODO # Example: d32b55bdf1737ec415df8c88b3bf91e194b59ee3127e3f38ea46fd88ba2e7849 ``` </TabItem> @@ -141,7 +141,7 @@ Open a terminal or console to the `iota` directory you downloaded in the previou </TabItem> </Tabs> - + 1. Optional: Skip this step to accept the default paths to resources. Edit the fullnode.yaml file to use custom paths. 1. Update the `db-path` field with the path to the Full node database. `db-path: "/db-files/iota-fullnode"` @@ -159,7 +159,7 @@ Run the following command to compile the `iota-node`. cargo run --release --bin iota-node ``` -### Starting services +### Starting services At this point, your IOTA Full node is ready to connect to the IOTA network. @@ -172,7 +172,7 @@ If your setup is successful, your IOTA Full node is now connected to the appropr Your Full node serves the read endpoints of the IOTA JSON-RPC API at: `http://127.0.0.1:9000`. -### Troubleshooting +### Troubleshooting If, during the compilation step, you receive a `cannot find -lpq` error, you are missing the `libpq` library. Use `sudo apt-get install libpq-dev` to install on Linux, or `brew install libpq` on MacOS. After you install on MacOS, create a Homebrew link using `brew link --force libpq`. For further context, reference the [issue on Stack Overflow](https://stackoverflow.com/questions/70313347/ld-library-not-found-for-lpq-when-build-rust-in-macos?rq=1). If you receive the following error: @@ -187,17 +187,17 @@ Then update the metrics address in your fullnode.yaml file to use port `9180`. metrics-address: "0.0.0.0:9180" ``` -## Monitoring +## Monitoring Monitor your Full node using the instructions in the _Node Monitoring and Metrics_ section. The default metrics port is `9184`. To change the port, edit your `fullnode.yaml` file. -## Update your Full node +## Update your Full node Whenever IOTA releases a new version, you must update your Full node with the release to ensure compatibility with the network it connects to. For example, if you use IOTA Testnet you should install the version of IOTA running on IOTA Testnet. -### Update with Docker Compose +### Update with Docker Compose Follow the instructions to [reset the environment](https://github.com/iotaledger/iota/tree/develop/docker/fullnode#reset-the-environment), namely by running the command: @@ -205,7 +205,7 @@ Follow the instructions to [reset the environment](https://github.com/iotaledger docker-compose down --volumes ``` -### Update from source +### Update from source If you followed the instructions for Building from Source, use the following steps to update your Full node: @@ -241,13 +241,13 @@ If you followed the instructions for Building from Source, use the following ste ```shell ./target/release/iota-node --config-path fullnode.yaml ``` - + Your Full node starts on: http://127.0.0.1:9000. ## Object pruning {#object-pruning} -IOTA adds new object versions to the database as part of transaction execution. This makes previous versions ready for -garbage collection. However, without pruning, this can result in database performance degradation and requires large +IOTA adds new object versions to the database as part of transaction execution. This makes previous versions ready for +garbage collection. However, without pruning, this can result in database performance degradation and requires large amounts of storage space. IOTA identifies the objects that are eligible for pruning in each checkpoint, and then performs the pruning in the background. @@ -255,7 +255,7 @@ You can enable pruning for an IOTA node by adding the `authority-store-pruning-c ```yaml authority-store-pruning-config: - # Number of epoch dbs to keep + # Number of epoch dbs to keep # Not relevant for object pruning num-latest-epoch-dbs-to-retain: 3 # The amount of time, in seconds, between running the object pruning task. @@ -264,16 +264,16 @@ authority-store-pruning-config: # Number of epochs to wait before performing object pruning. # When set to 0, IOTA prunes old object versions as soon # as possible. This is also called *aggressive pruning*, and results in the most effective - # garbage collection method with the lowest disk usage possible. + # garbage collection method with the lowest disk usage possible. # This is the recommended setting for IOTA Validator nodes since older object versions aren't # necessary to execute transactions. # When set to 1, IOTA prunes only object versions from transaction checkpoints - # previous to the current epoch. In general, when set to N (where N >= 1), IOTA prunes - # only object versions from checkpoints up to `current - N` epoch. - # It is therefore possible to have multiple versions of an object present - # in the database. This setting is recommended for IOTA Full nodes as they might need to serve + # previous to the current epoch. In general, when set to N (where N >= 1), IOTA prunes + # only object versions from checkpoints up to `current - N` epoch. + # It is therefore possible to have multiple versions of an object present + # in the database. This setting is recommended for IOTA Full nodes as they might need to serve # RPC requests that require looking up objects by ID and Version (rather than just the latest - # version). However, if your Full node does not serve RPC requests you should then also enable + # version). However, if your Full node does not serve RPC requests you should then also enable # aggressive pruning. num-epochs-to-retain: 0 # Advanced setting: Maximum number of checkpoints to prune in a batch. The default @@ -301,9 +301,9 @@ authority-store-pruning-config: max-checkpoints-in-batch: 10 max-transactions-in-batch: 1000 # Number of epochs to wait before performing transaction pruning. - # When this is N (where N >= 2), IOTA prunes transactions and effects from + # When this is N (where N >= 2), IOTA prunes transactions and effects from # checkpoints up to the `current - N` epoch. IOTA never prunes transactions and effects from the current and - # immediately prior epoch. N = 2 is a recommended setting for IOTA Validator nodes and IOTA Full nodes that don't + # immediately prior epoch. N = 2 is a recommended setting for IOTA Validator nodes and IOTA Full nodes that don't # serve RPC requests. num-epochs-to-retain-for-checkpoints: 2 # Ensures that individual database files periodically go through the compaction process. diff --git a/docs/content/references/cli/console.mdx b/docs/content/references/cli/console.mdx index 52b9ef9f7c6..3b433fcc6a7 100644 --- a/docs/content/references/cli/console.mdx +++ b/docs/content/references/cli/console.mdx @@ -38,8 +38,8 @@ Available RPC methods: ["iota_devInspectTransactionBlock", "iota_dryRunTransacti "iota_tryGetPastObject", "iota_tryMultiGetPastObjects", "iotax_getAllBalances", "iotax_getAllCoins", "iotax_getBalance", "iotax_getCoinMetadata", "iotax_getCoins", "iotax_getCommitteeInfo", "iotax_getDynamicFieldObject", "iotax_getDynamicFields", "iotax_getLatestIOTASystemState", "iotax_getOwnedObjects", "iotax_getReferenceGasPrice", "iotax_getStakes", "iotax_getStakesByIds", -"iotax_getTotalSupply", "iotax_getValidatorsApy", "iotax_queryEvents", "iotax_queryTransactionBlocks", "iotax_subscribeEvent", -"iotax_subscribeTransaction", "unsafe_batchTransaction", "unsafe_mergeCoins", +"iotax_getTotalSupply", "iotax_getValidatorsApy", "iotax_queryEvents", "iotax_queryTransactionBlocks", "iotax_resolveNameServiceAddress", +"iotax_resolveNameServiceNames", "iotax_subscribeEvent", "iotax_subscribeTransaction", "unsafe_batchTransaction", "unsafe_mergeCoins", "unsafe_moveCall", "unsafe_pay", "unsafe_payAllIOTA", "unsafe_payIOTA", "unsafe_publish", "unsafe_requestAddStake", "unsafe_requestWithdrawStake", "unsafe_splitCoin", "unsafe_splitCoinEqual", "unsafe_transferIOTA", "unsafe_transferObject"] diff --git a/docs/content/references/contribute/contribute-to-iota-repos.mdx b/docs/content/references/contribute/contribute-to-iota-repos.mdx index 9c4a678c7f8..134e31484b6 100644 --- a/docs/content/references/contribute/contribute-to-iota-repos.mdx +++ b/docs/content/references/contribute/contribute-to-iota-repos.mdx @@ -18,3 +18,8 @@ To report an issue with IOTA, [create an issue](https://github.com/iotaledger/io To contribute to IOTA source code or documentation, you need only a GitHub account. You can commit updates and then submit a PR directly from the Github website, or create a fork of the repo to your local environment and use your favorite tools to make changes. Always submit PRs to the `main` branch. See [IOTA Environment Setup](/developer/getting-started/iota-environment.mdx) for instructions on forking the IOTA repository, if necessary. + +## Contribute via the IOTA Improvement Proposal (SIP) process {#SIP} + +The IOTA Network is an open-source, decentralized, and permissionless protocol that welcomes community contributions. If you have an idea regarding a core protocol upgrade, the best way to make your voice heard is via submitting a IOTA Improvement Proposal (SIP). For more information on SIPs, see [Contribute to IOTA through SIPs](https://blog.iota.io/iota-improvement-proposals-sips/). + diff --git a/docs/content/references/contribute/localize-iota-docs.mdx b/docs/content/references/contribute/localize-iota-docs.mdx new file mode 100644 index 00000000000..3827502075e --- /dev/null +++ b/docs/content/references/contribute/localize-iota-docs.mdx @@ -0,0 +1,6 @@ +--- +title: Localize IOTA Documentation +slug: /localize-iota-docs +--- + +The IOTA documentation can be localized (translated) into any language of your choosing. The localization platform utilized is Crowdin. For more information regarding the localization process please see [here](https://support.crowdin.com/crowdin-intro/). diff --git a/docs/content/references/iota-glossary.mdx b/docs/content/references/iota-glossary.mdx index f39220836b4..75929f62875 100644 --- a/docs/content/references/iota-glossary.mdx +++ b/docs/content/references/iota-glossary.mdx @@ -75,7 +75,11 @@ A [smart contract](https://en.wikipedia.org/wiki/Smart_contract) is an agreement ### IOTA {#iota} -IOTA refers to the IOTA blockchain, and the [IOTA open source project](https://github.com/iotaledger/iota/) as a whole, or the native token to the IOTA network. +IOTA refers to the IOTA blockchain, and the [IOTA open source project](https://github.com/iotaledger/iota/) as a whole. + +### IOTA {#iota-1} + +IOTA is the native token to the IOTA network. ### Total order {#total-order} diff --git a/docs/content/references/ts-sdk/dapp-kit/rpc-hooks.mdx b/docs/content/references/ts-sdk/dapp-kit/rpc-hooks.mdx index 193c3fc7c75..9506f13f189 100644 --- a/docs/content/references/ts-sdk/dapp-kit/rpc-hooks.mdx +++ b/docs/content/references/ts-sdk/dapp-kit/rpc-hooks.mdx @@ -145,3 +145,25 @@ function MyComponent() { ); } ``` + +## useResolveIotaNSName + +To get the IotaNS name for a given address, use the `useResolveIotaNSName` hook. + +```tsx +import { useResolveIotaNSName } from '@iota/dapp-kit'; + +function MyComponent() { + const { data, isPending } = useResolveIotaNSName('0x123'); + + if (isPending) { + return <div>Loading...</div>; + } + + if (data) { + return <div>Domain name is: {data}</div>; + } + + return <div>Domain name not found</div>; +} +``` diff --git a/docs/content/sidebars/about-iota.js b/docs/content/sidebars/about-iota.js index f618bc0d3e9..4f61f32d80c 100644 --- a/docs/content/sidebars/about-iota.js +++ b/docs/content/sidebars/about-iota.js @@ -39,5 +39,24 @@ const aboutIota = [ 'about-iota/tokenomics/gas-pricing', ], }, + { + type: 'category', + label: 'Expert topics', + items: [ + { + type: 'category', + label: 'Execution Architecture', + link: { + type: 'doc', + id: 'about-iota/execution-architecture/execution-layer', + }, + items: [ + 'about-iota/execution-architecture/iota-execution', + 'about-iota/execution-architecture/adapter', + 'about-iota/execution-architecture/natives', + ], + }, + ], + }, ]; module.exports = aboutIota; diff --git a/docs/content/sidebars/references.js b/docs/content/sidebars/references.js index dd1d40015fd..fcac9fa935f 100644 --- a/docs/content/sidebars/references.js +++ b/docs/content/sidebars/references.js @@ -323,25 +323,6 @@ const references = [ }, ], }, - { - type: 'category', - label: 'Expert topics', - items: [ - { - type: 'category', - label: 'Execution Architecture', - link: { - type: 'doc', - id: 'references/execution-architecture/execution-layer', - }, - items: [ - 'references/execution-architecture/iota-execution', - 'references/execution-architecture/adapter', - 'references/execution-architecture/natives', - ], - }, - ], - }, 'references/research-papers', 'references/iota-glossary', { @@ -353,6 +334,13 @@ const references = [ }, items: [ 'references/contribute/contribution-process', + 'references/contribute/contribute-to-iota-repos', + { + type: 'link', + label: 'Submit a SIP', + href: 'https://sips.iota.io', + }, + 'references/contribute/localize-iota-docs', 'references/contribute/code-of-conduct', 'references/contribute/style-guide', ], diff --git a/nre/validator_tool.md b/nre/validator_tool.md index cbeb008c435..ed512653a2e 100644 --- a/nre/validator_tool.md +++ b/nre/validator_tool.md @@ -169,8 +169,6 @@ At this point you are validator candidate and can start to accept self staking a **If you haven't, start a fullnode now to catch up with the network. When you officially join the committee but is not fully up-to-date, you cannot make meaningful contribution to the network and may be subject to peer reporting hence face the risk of reduced staking rewards for you and your delegators.** -Add stake to a validator's staking pool: https://docs.iota.org/references/framework/iota-system/iota_system#function-request_add_stake - Once you collect enough staking amount, run ```bash diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eea713d4a6b..3dd5c2e8653 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1107,8 +1107,8 @@ importers: specifier: ^3.5.0 version: 3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: - specifier: 14.2.10 - version: 14.2.10(@babel/core@7.23.9)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.63.6) + specifier: 14.2.3 + version: 14.2.3(@babel/core@7.23.9)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.63.6) react: specifier: ^18.3.1 version: 18.3.1 @@ -1866,6 +1866,31 @@ importers: specifier: ^7.0.1 version: 7.0.1(debug@4.3.4) + sdk/iotans-toolkit: + dependencies: + '@iota/iota-sdk': + specifier: workspace:* + version: link:../typescript + devDependencies: + '@faker-js/faker': + specifier: ^8.0.2 + version: 8.0.2 + '@iota/build-scripts': + specifier: workspace:* + version: link:../build-scripts + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@swc/core@1.3.92)(@types/node@20.4.2)(typescript@5.3.3) + typescript: + specifier: ^5.3.3 + version: 5.3.3 + vitest: + specifier: ^0.33.0 + version: 0.33.0(@vitest/ui@0.33.0)(happy-dom@10.5.1)(jsdom@23.0.0)(playwright@1.46.1)(sass@1.63.6)(terser@5.31.0) + sdk/kiosk: dependencies: '@iota/iota-sdk': @@ -4358,6 +4383,10 @@ packages: engines: {node: '>=18'} hasBin: true + '@faker-js/faker@8.0.2': + resolution: {integrity: sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + '@fal-works/esbuild-plugin-global-externals@2.1.2': resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} @@ -5225,62 +5254,62 @@ packages: '@nestjs/platform-express': optional: true - '@next/env@14.2.10': - resolution: {integrity: sha512-dZIu93Bf5LUtluBXIv4woQw2cZVZ2DJTjax5/5DOs3lzEOeKLy7GxRSr4caK9/SCPdaW6bCgpye6+n4Dh9oJPw==} + '@next/env@14.2.3': + resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} '@next/eslint-plugin-next@14.2.3': resolution: {integrity: sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==} - '@next/swc-darwin-arm64@14.2.10': - resolution: {integrity: sha512-V3z10NV+cvMAfxQUMhKgfQnPbjw+Ew3cnr64b0lr8MDiBJs3eLnM6RpGC46nhfMZsiXgQngCJKWGTC/yDcgrDQ==} + '@next/swc-darwin-arm64@14.2.3': + resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.10': - resolution: {integrity: sha512-Y0TC+FXbFUQ2MQgimJ/7Ina2mXIKhE7F+GUe1SgnzRmwFY3hX2z8nyVCxE82I2RicspdkZnSWMn4oTjIKz4uzA==} + '@next/swc-darwin-x64@14.2.3': + resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.10': - resolution: {integrity: sha512-ZfQ7yOy5zyskSj9rFpa0Yd7gkrBnJTkYVSya95hX3zeBG9E55Z6OTNPn1j2BTFWvOVVj65C3T+qsjOyVI9DQpA==} + '@next/swc-linux-arm64-gnu@14.2.3': + resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.10': - resolution: {integrity: sha512-n2i5o3y2jpBfXFRxDREr342BGIQCJbdAUi/K4q6Env3aSx8erM9VuKXHw5KNROK9ejFSPf0LhoSkU/ZiNdacpQ==} + '@next/swc-linux-arm64-musl@14.2.3': + resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.10': - resolution: {integrity: sha512-GXvajAWh2woTT0GKEDlkVhFNxhJS/XdDmrVHrPOA83pLzlGPQnixqxD8u3bBB9oATBKB//5e4vpACnx5Vaxdqg==} + '@next/swc-linux-x64-gnu@14.2.3': + resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.10': - resolution: {integrity: sha512-opFFN5B0SnO+HTz4Wq4HaylXGFV+iHrVxd3YvREUX9K+xfc4ePbRrxqOuPOFjtSuiVouwe6uLeDtabjEIbkmDA==} + '@next/swc-linux-x64-musl@14.2.3': + resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.10': - resolution: {integrity: sha512-9NUzZuR8WiXTvv+EiU/MXdcQ1XUvFixbLIMNQiVHuzs7ZIFrJDLJDaOF1KaqttoTujpcxljM/RNAOmw1GhPPQQ==} + '@next/swc-win32-arm64-msvc@14.2.3': + resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.10': - resolution: {integrity: sha512-fr3aEbSd1GeW3YUMBkWAu4hcdjZ6g4NBl1uku4gAn661tcxd1bHs1THWYzdsbTRLcCKLjrDZlNp6j2HTfrw+Bg==} + '@next/swc-win32-ia32-msvc@14.2.3': + resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.10': - resolution: {integrity: sha512-UjeVoRGKNL2zfbcQ6fscmgjBAS/inHBh63mjIlfPg/NG8Yn2ztqylXt5qilYb6hoHIwaU2ogHknHWWmahJjgZQ==} + '@next/swc-win32-x64-msvc@14.2.3': + resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -8946,6 +8975,9 @@ packages: caniuse-lite@1.0.30001520: resolution: {integrity: sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA==} + caniuse-lite@1.0.30001585: + resolution: {integrity: sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==} + caniuse-lite@1.0.30001636: resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} @@ -13429,8 +13461,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@14.2.10: - resolution: {integrity: sha512-sDDExXnh33cY3RkS9JuFEKaS4HmlWmDKP1VJioucCG6z5KuA008DPsDZOzi8UfqEk3Ii+2NCQSJrfbEWtZZfww==} + next@14.2.3: + resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -21449,6 +21481,8 @@ snapshots: '@ethereumjs/rlp@5.0.2': {} + '@faker-js/faker@8.0.2': {} + '@fal-works/esbuild-plugin-global-externals@2.1.2': {} '@floating-ui/core@1.3.1': {} @@ -22864,37 +22898,37 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8) - '@next/env@14.2.10': {} + '@next/env@14.2.3': {} '@next/eslint-plugin-next@14.2.3': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@14.2.10': + '@next/swc-darwin-arm64@14.2.3': optional: true - '@next/swc-darwin-x64@14.2.10': + '@next/swc-darwin-x64@14.2.3': optional: true - '@next/swc-linux-arm64-gnu@14.2.10': + '@next/swc-linux-arm64-gnu@14.2.3': optional: true - '@next/swc-linux-arm64-musl@14.2.10': + '@next/swc-linux-arm64-musl@14.2.3': optional: true - '@next/swc-linux-x64-gnu@14.2.10': + '@next/swc-linux-x64-gnu@14.2.3': optional: true - '@next/swc-linux-x64-musl@14.2.10': + '@next/swc-linux-x64-musl@14.2.3': optional: true - '@next/swc-win32-arm64-msvc@14.2.10': + '@next/swc-win32-arm64-msvc@14.2.3': optional: true - '@next/swc-win32-ia32-msvc@14.2.10': + '@next/swc-win32-ia32-msvc@14.2.3': optional: true - '@next/swc-win32-x64-msvc@14.2.10': + '@next/swc-win32-x64-msvc@14.2.3': optional: true '@nicolo-ribaudo/semver-v6@6.3.3': {} @@ -28106,6 +28140,8 @@ snapshots: caniuse-lite@1.0.30001520: {} + caniuse-lite@1.0.30001585: {} + caniuse-lite@1.0.30001636: {} capital-case@1.0.4: @@ -34331,27 +34367,27 @@ snapshots: neo-async@2.6.2: {} - next@14.2.10(@babel/core@7.23.9)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.63.6): + next@14.2.3(@babel/core@7.23.9)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.63.6): dependencies: - '@next/env': 14.2.10 + '@next/env': 14.2.3 '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001636 + caniuse-lite: 1.0.30001585 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.23.9)(babel-plugin-macros@3.1.0)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.10 - '@next/swc-darwin-x64': 14.2.10 - '@next/swc-linux-arm64-gnu': 14.2.10 - '@next/swc-linux-arm64-musl': 14.2.10 - '@next/swc-linux-x64-gnu': 14.2.10 - '@next/swc-linux-x64-musl': 14.2.10 - '@next/swc-win32-arm64-msvc': 14.2.10 - '@next/swc-win32-ia32-msvc': 14.2.10 - '@next/swc-win32-x64-msvc': 14.2.10 + '@next/swc-darwin-arm64': 14.2.3 + '@next/swc-darwin-x64': 14.2.3 + '@next/swc-linux-arm64-gnu': 14.2.3 + '@next/swc-linux-arm64-musl': 14.2.3 + '@next/swc-linux-x64-gnu': 14.2.3 + '@next/swc-linux-x64-musl': 14.2.3 + '@next/swc-win32-arm64-msvc': 14.2.3 + '@next/swc-win32-ia32-msvc': 14.2.3 + '@next/swc-win32-x64-msvc': 14.2.3 '@playwright/test': 1.46.1 sass: 1.63.6 transitivePeerDependencies: diff --git a/sdk/dapp-kit/src/components/AccountDropdownMenu.tsx b/sdk/dapp-kit/src/components/AccountDropdownMenu.tsx index af5b23924e8..cb242c23397 100644 --- a/sdk/dapp-kit/src/components/AccountDropdownMenu.tsx +++ b/sdk/dapp-kit/src/components/AccountDropdownMenu.tsx @@ -7,6 +7,7 @@ import type { WalletAccount } from '@iota/wallet-standard'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import clsx from 'clsx'; +import { useResolveIotaNSName } from '../hooks/useResolveIotaNSNames.js'; import { useAccounts } from '../hooks/wallet/useAccounts.js'; import { useDisconnectWallet } from '../hooks/wallet/useDisconnectWallet.js'; import { useSwitchAccount } from '../hooks/wallet/useSwitchAccount.js'; @@ -23,6 +24,10 @@ type AccountDropdownMenuProps = { export function AccountDropdownMenu({ currentAccount }: AccountDropdownMenuProps) { const { mutate: disconnectWallet } = useDisconnectWallet(); + + const { data: domain } = useResolveIotaNSName( + currentAccount.label ? null : currentAccount.address, + ); const accounts = useAccounts(); return ( @@ -31,7 +36,9 @@ export function AccountDropdownMenu({ currentAccount }: AccountDropdownMenuProps <DropdownMenu.Trigger asChild> <Button size="lg" className={styles.connectedAccount}> <Text mono weight="bold"> - {currentAccount.label ?? formatAddress(currentAccount.address)} + {currentAccount.label ?? + domain ?? + formatAddress(currentAccount.address)} </Text> <ChevronIcon /> </Button> @@ -69,12 +76,14 @@ export function AccountDropdownMenuItem({ active?: boolean; }) { const { mutate: switchAccount } = useSwitchAccount(); + const { data: domain } = useResolveIotaNSName(account.label ? null : account.address); + return ( <DropdownMenu.Item className={clsx(styles.menuItem, styles.switchAccountMenuItem)} onSelect={() => switchAccount({ account })} > - <Text mono>{account.label ?? formatAddress(account.address)}</Text> + <Text mono>{account.label ?? domain ?? formatAddress(account.address)}</Text> {active ? <CheckIcon /> : null} </DropdownMenu.Item> ); diff --git a/sdk/dapp-kit/src/hooks/useResolveIotaNSNames.ts b/sdk/dapp-kit/src/hooks/useResolveIotaNSNames.ts new file mode 100644 index 00000000000..b44bbfe8247 --- /dev/null +++ b/sdk/dapp-kit/src/hooks/useResolveIotaNSNames.ts @@ -0,0 +1,31 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import type { ResolvedNameServiceNames } from '@iota/iota-sdk/client'; +import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; + +import { useIotaClientQuery } from './useIotaClientQuery.js'; + +export function useResolveIotaNSName( + address?: string | null, + options?: Omit< + UseQueryOptions<ResolvedNameServiceNames, Error, string | null, unknown[]>, + 'queryFn' | 'queryKey' | 'select' + >, +): UseQueryResult<string | null, Error> { + return useIotaClientQuery( + 'resolveNameServiceNames', + { + address: address!, + limit: 1, + }, + { + ...options, + refetchOnWindowFocus: false, + retry: false, + select: (data) => (data.data.length > 0 ? data.data[0] : null), + enabled: !!address && options?.enabled !== false, + }, + ); +} diff --git a/sdk/dapp-kit/src/index.ts b/sdk/dapp-kit/src/index.ts index 0868413591e..c1211e4489c 100644 --- a/sdk/dapp-kit/src/index.ts +++ b/sdk/dapp-kit/src/index.ts @@ -7,6 +7,7 @@ export * from './components/ConnectButton.js'; export * from './components/IotaClientProvider.js'; export * from './components/WalletProvider.js'; export * from './hooks/networkConfig.js'; +export * from './hooks/useResolveIotaNSNames.js'; export * from './hooks/useIotaClient.js'; export * from './hooks/useIotaClientInfiniteQuery.js'; export * from './hooks/useIotaClientMutation.js'; diff --git a/sdk/iotans-toolkit/README.md b/sdk/iotans-toolkit/README.md new file mode 100644 index 00000000000..7661adb7791 --- /dev/null +++ b/sdk/iotans-toolkit/README.md @@ -0,0 +1,81 @@ +# IotaNS TypeScript SDK + +This is a lightweight SDK (1kB minified bundle size), providing utility classes and functions for +applications to interact with on-chain `.iota` names registered from +[Iota Name Service (iotans.io)](https://iotans.io). + +## Getting started + +The SDK is published to [npm registry](https://www.npmjs.com/package/@iota/iotans-toolkit). To use +it in your project: + +```bash +$ npm install @iota/iotans-toolkit +``` + +You can also use yarn or pnpm. + +## Examples + +Create an instance of IotansClient: + +```typescript +import { IotaClient } from '@iota/iota-sdk/client'; +import { IotansClient } from '@iota/iotans-toolkit'; + +const client = new IotaClient(); +export const iotansClient = new IotansClient(client); +``` + +Choose network type: + +```typescript +export const iotansClient = new IotansClient(client, { + networkType: 'testnet', +}); +``` + +> **Note:** To ensure best performance, please make sure to create only one instance of the +> IotansClient class in your application. Then, import the created `iotansClient` instance to use +> its functions. + +Fetch an address linked to a name: + +```typescript +const address = await iotansClient.getAddress('iotans.iota'); +``` + +Fetch the default name of an address: + +```typescript +const defaultName = await iotansClient.getName( + '0xc2f08b6490b87610629673e76bab7e821fe8589c7ea6e752ea5dac2a4d371b41', +); +``` + +Fetch a name object: + +```typescript +const nameObject = await iotansClient.getNameObject('iotans.iota'); +``` + +Fetch a name object including the owner: + +```typescript +const nameObject = await iotansClient.getNameObject('iotans.iota', { + showOwner: true, +}); +``` + +Fetch a name object including the Avatar the owner has set (it automatically includes owner too): + +```typescript +const nameObject = await iotansClient.getNameObject('iotans.iota', { + showOwner: true, // this can be skipped as showAvatar includes it by default + showAvatar: true, +}); +``` + +## License + +[Apache-2.0](https://github.com/IotaNSdapp/toolkit/blob/main/LICENSE) diff --git a/sdk/iotans-toolkit/package.json b/sdk/iotans-toolkit/package.json new file mode 100644 index 00000000000..5ff20944776 --- /dev/null +++ b/sdk/iotans-toolkit/package.json @@ -0,0 +1,49 @@ +{ + "name": "@iota/iotans-toolkit", + "private": true, + "author": "IOTA Foundation <contact@iota.org>", + "description": "IotaNS TypeScript SDK", + "version": "0.0.0", + "license": "Apache-2.0", + "type": "commonjs", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/cjs/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + } + }, + "files": [ + "CHANGELOG.md", + "dist", + "src" + ], + "engines": { + "node": ">=20" + }, + "scripts": { + "clean": "rm -rf tsconfig.tsbuildinfo ./dist", + "build": "build-package", + "prepublishOnly": "pnpm build", + "test": "vitest", + "prettier:check": "prettier -c --ignore-unknown --ignore-path=../../.prettierignore --ignore-path=.prettierignore .", + "prettier:fix": "prettier -w --ignore-unknown --ignore-path=../../.prettierignore --ignore-path=.prettierignore .", + "eslint:check": "eslint --max-warnings=0 .", + "eslint:fix": "pnpm run eslint:check --fix", + "lint": "pnpm run eslint:check && pnpm run prettier:check", + "lint:fix": "pnpm run eslint:fix && pnpm run prettier:fix" + }, + "devDependencies": { + "@faker-js/faker": "^8.0.2", + "@iota/build-scripts": "workspace:*", + "dotenv": "^16.4.5", + "ts-node": "^10.9.1", + "typescript": "^5.3.3", + "vitest": "^0.33.0" + }, + "dependencies": { + "@iota/iota-sdk": "workspace:*" + } +} diff --git a/sdk/iotans-toolkit/src/client.ts b/sdk/iotans-toolkit/src/client.ts new file mode 100644 index 00000000000..ae9e953e5b5 --- /dev/null +++ b/sdk/iotans-toolkit/src/client.ts @@ -0,0 +1,199 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import type { IotaClient } from '@iota/iota-sdk/client'; + +import type { DataFields, NameObject, NetworkType, IotaNSContract } from './types/objects.js'; +import { DEVNET_JSON_FILE, GCS_URL, TESTNET_JSON_FILE } from './utils/constants.js'; +import { camelCase, parseObjectDataResponse, parseRegistryResponse } from './utils/parser.js'; +import { getAvatar, getOwner } from './utils/queries.js'; + +export const AVATAR_NOT_OWNED = 'AVATAR_NOT_OWNED'; + +class IotansClient { + private iotaClient: IotaClient; + contractObjects: IotaNSContract | undefined; + networkType: NetworkType | undefined; + + constructor( + iotaClient: IotaClient, + options?: { + contractObjects?: IotaNSContract; + networkType?: NetworkType; + }, + ) { + if (!iotaClient) { + throw new Error('IotaClient must be specified.'); + } + this.iotaClient = iotaClient; + this.contractObjects = options?.contractObjects; + this.networkType = options?.networkType; + } + + async getIotansContractObjects() { + if ((this.contractObjects as IotaNSContract)?.packageId) return; + + const contractJsonFileUrl = + GCS_URL + (this.networkType === 'testnet' ? TESTNET_JSON_FILE : DEVNET_JSON_FILE); + + let response; + try { + response = await fetch(contractJsonFileUrl); + } catch (error) { + throw new Error(`Error getting IotaNS contract objects, ${(error as Error).message}`); + } + + if (!response?.ok) { + throw new Error(`Network Error: ${response?.status}`); + } + + this.contractObjects = await response.json(); + } + + protected async getDynamicFieldObject( + parentObjectId: string, + key: unknown, + type = '0x1::string::String', + ) { + const dynamicFieldObject = await this.iotaClient.getDynamicFieldObject({ + parentId: parentObjectId, + name: { + type: type, + value: key, + }, + }); + + if (dynamicFieldObject.error?.code === 'dynamicFieldNotFound') return; + + return dynamicFieldObject; + } + + protected async getNameData(dataObjectId: string, fields: DataFields[] = []) { + if (!dataObjectId) return {}; + + const { data: dynamicFields } = await this.iotaClient.getDynamicFields({ + parentId: dataObjectId, + }); + + const filteredFields = new Set(fields); + const filteredDynamicFields = dynamicFields.filter(({ name: { value } }) => + filteredFields.has(value as DataFields), + ); + + const data = await Promise.allSettled( + filteredDynamicFields?.map(({ objectId }) => + this.iotaClient + .getObject({ + id: objectId, + options: { showContent: true }, + }) + .then(parseObjectDataResponse) + .then((object) => [camelCase(object.name), object.value]), + ) ?? [], + ); + + const fulfilledData = data.filter( + (e) => e.status === 'fulfilled', + ) as PromiseFulfilledResult<[string, unknown]>[]; + + return Object.fromEntries(fulfilledData.map((e) => e.value)); + } + + /** + * Returns the name object data including: + * + * - id: the name object address + * - owner: the owner address // only if you add the `showOwner` parameter. It includes an extra RPC call. + * - targetAddress: the linked address + * - avatar?: the custom avatar id // Only if you add showAvatar parameter. It includes an extra RPC call. + * - contentHash?: the ipfs cid + * + * If the input domain has not been registered, it will return an empty object. + * If `showAvatar` is included, the owner will be fetched as well. + * + * @param key a domain name + */ + async getNameObject( + name: string, + options: { showOwner?: boolean; showAvatar?: boolean } | undefined = { + showOwner: false, + showAvatar: false, + }, + ): Promise<NameObject> { + const [, domain, topLevelDomain] = name.match(/^(.+)\.([^.]+)$/) || []; + await this.getIotansContractObjects(); + + const registryResponse = await this.getDynamicFieldObject( + (this.contractObjects as IotaNSContract).registry, + [topLevelDomain, domain], + `${this.contractObjects?.packageId}::domain::Domain`, + ); + + const nameObject = parseRegistryResponse(registryResponse); + + // check if we should also query for avatar. + // we can only query if the object has an avatar set + // and the query includes avatar. + const includeAvatar = nameObject.avatar && options?.showAvatar; + + // IF we have showOwner or includeAvatar flag, we fetch the owner &/or avatar, + // We use Promise.all to do these calls at the same time. + if (nameObject.nftId && (includeAvatar || options?.showOwner)) { + const [owner, avatarNft] = await Promise.all([ + getOwner(this.iotaClient, nameObject.nftId), + includeAvatar + ? getAvatar(this.iotaClient, nameObject.avatar) + : Promise.resolve(null), + ]); + + nameObject.owner = owner; + + // Parse avatar NFT, check ownership and fixup the request response. + if (includeAvatar && avatarNft) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-next-line + if (avatarNft.data?.owner?.AddressOwner === nameObject.owner) { + const display = avatarNft.data?.display; + nameObject.avatar = display?.data?.image_url || null; + } else { + nameObject.avatar = AVATAR_NOT_OWNED; + } + } else { + delete nameObject.avatar; + } + } + + return nameObject; + } + + /** + * Returns the linked address of the input domain if the link was set. Otherwise, it will return undefined. + * + * @param domain a domain name ends with `.iota` + */ + async getAddress(domain: string): Promise<string | undefined> { + const { targetAddress } = await this.getNameObject(domain); + + return targetAddress; + } + + /** + * Returns the default name of the input address if it was set. Otherwise, it will return undefined. + * + * @param address a Iota address. + */ + async getName(address: string): Promise<string | undefined> { + const res = await this.getDynamicFieldObject( + this.contractObjects?.reverseRegistry ?? '', + address, + 'address', + ); + const data = parseObjectDataResponse(res); + const labels = data?.value?.fields?.labels; + + return Array.isArray(labels) ? labels.reverse()?.join('.') : undefined; + } +} + +export { IotansClient }; diff --git a/sdk/iotans-toolkit/src/index.ts b/sdk/iotans-toolkit/src/index.ts new file mode 100644 index 00000000000..c908995d36d --- /dev/null +++ b/sdk/iotans-toolkit/src/index.ts @@ -0,0 +1,7 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './client.js'; +export * from './types/index.js'; +export * from './utils/parser.js'; diff --git a/sdk/iotans-toolkit/src/types/index.ts b/sdk/iotans-toolkit/src/types/index.ts new file mode 100644 index 00000000000..0f5be1e81f9 --- /dev/null +++ b/sdk/iotans-toolkit/src/types/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './objects.js'; diff --git a/sdk/iotans-toolkit/src/types/objects.ts b/sdk/iotans-toolkit/src/types/objects.ts new file mode 100644 index 00000000000..3a1e9a5322a --- /dev/null +++ b/sdk/iotans-toolkit/src/types/objects.ts @@ -0,0 +1,22 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export type IotaNSContract = { + packageId: string; + iotans: string; + registry: string; + reverseRegistry: string; +}; + +export type NameObject = { + id: string; + owner: string; + targetAddress: string; + avatar?: string; + contentHash?: string; +}; + +export type DataFields = 'avatar' | 'contentHash'; + +export type NetworkType = 'devnet' | 'testnet'; diff --git a/sdk/iotans-toolkit/src/utils/constants.ts b/sdk/iotans-toolkit/src/utils/constants.ts new file mode 100644 index 00000000000..c94dd013eea --- /dev/null +++ b/sdk/iotans-toolkit/src/utils/constants.ts @@ -0,0 +1,7 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export const GCS_URL = 'https://storage.googleapis.com/iotans-data/'; +export const DEVNET_JSON_FILE = 'contract-devnet.json'; +export const TESTNET_JSON_FILE = 'contract-testnet.json'; diff --git a/sdk/iotans-toolkit/src/utils/parser.ts b/sdk/iotans-toolkit/src/utils/parser.ts new file mode 100644 index 00000000000..5175672a843 --- /dev/null +++ b/sdk/iotans-toolkit/src/utils/parser.ts @@ -0,0 +1,43 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import type { IotaMoveObject, IotaObjectData, IotaObjectResponse } from '@iota/iota-sdk/client'; +import { normalizeIotaAddress } from '@iota/iota-sdk/utils'; + +export const camelCase = (string: string) => string.replace(/(_\w)/g, (g) => g[1].toUpperCase()); + +export const parseObjectDataResponse = (response: IotaObjectResponse | undefined) => + ((response?.data as IotaObjectData)?.content as IotaMoveObject)?.fields as Record<string, any>; + +export const parseRegistryResponse = (response: IotaObjectResponse | undefined): any => { + const fields = parseObjectDataResponse(response)?.value?.fields || {}; + + const object = Object.fromEntries( + Object.entries({ ...fields }).map(([key, val]) => [camelCase(key), val]), + ); + + if (response?.data?.objectId) { + object.id = response.data.objectId; + } + + delete object.data; + + const data = (fields.data?.fields.contents || []).reduce( + (acc: Record<string, any>, c: Record<string, any>) => { + const key = c.fields.key; + const value = c.fields.value; + + return { + ...acc, + [camelCase(key)]: + c.type.includes('Address') || key === 'addr' + ? normalizeIotaAddress(value) + : value, + }; + }, + {}, + ); + + return { ...object, ...data }; +}; diff --git a/sdk/iotans-toolkit/src/utils/queries.ts b/sdk/iotans-toolkit/src/utils/queries.ts new file mode 100644 index 00000000000..5eae7bfca30 --- /dev/null +++ b/sdk/iotans-toolkit/src/utils/queries.ts @@ -0,0 +1,33 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import type { IotaClient, IotaObjectResponse } from '@iota/iota-sdk/client'; + +// get NFT's owner from RPC. +export const getOwner = async (client: IotaClient, nftId: string): Promise<string | null> => { + const ownerResponse = await client.getObject({ + id: nftId, + options: { showOwner: true }, + }); + const owner = ownerResponse.data?.owner; + return ( + (owner as { AddressOwner: string })?.AddressOwner || + (owner as { ObjectOwner: string })?.ObjectOwner || + null + ); +}; + +// get avatar NFT Object from RPC. +export const getAvatar = async ( + client: IotaClient, + avatar: string, +): Promise<IotaObjectResponse> => { + return await client.getObject({ + id: avatar, + options: { + showDisplay: true, + showOwner: true, + }, + }); +}; diff --git a/sdk/iotans-toolkit/tests/app.test.ts b/sdk/iotans-toolkit/tests/app.test.ts new file mode 100644 index 00000000000..74a12509fa0 --- /dev/null +++ b/sdk/iotans-toolkit/tests/app.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// import { faker } from '@faker-js/faker'; +import { getFullnodeUrl, IotaClient } from '@iota/iota-sdk/client'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { IotansClient } from '../src'; + +const domainName = 'test.iota'; +const walletAddress = '0xfce343a643991c592c4f1a9ee415a7889293f694ab8828f78e3c81d11c9530c6'; + +describe.skip('IotaNS Client', () => { + const client = new IotansClient(new IotaClient({ url: getFullnodeUrl('testnet') }), { + networkType: 'testnet', + contractObjects: { + packageId: '0xfdba31b34a43e058f17c5cf4b12d9b9e0a08c0623d8569092c022e0c77df46d3', + registry: '0xac06695279c2a92436068cebe5ea778135ac503337642e27493431603ae6a71d', + reverseRegistry: '0x34a36dd204f8351a157d19b87bada9d448ec40229d56f22bff04fa23713a5c31', + iotans: '0x4acaf19db12fafce1943bbd44c7f794e1d81d00aeb63617096e5caa39499ba88', + }, + }); + + const nonExistingDomain = walletAddress + '.iota'; + const nonExistingWalletAddress = walletAddress.substring(0, walletAddress.length - 4) + '0000'; + + beforeEach(async () => { + await client.getIotansContractObjects(); + }); + + describe('getAddress', () => { + describe('input domain has a linked address set', () => { + it('returns the linked address', async () => { + expect(await client.getAddress(domainName)).toEqual(walletAddress); + }); + }); + + describe('input domain does not have a linked address set', () => { + it('returns undefined', async () => { + expect(await client.getAddress(nonExistingDomain)).toBeUndefined(); + }); + }); + }); + + describe('getName', () => { + describe('input domain has a default name set', () => { + it('returns the default name', async () => { + expect(await client.getName(walletAddress)).toBe(domainName); + }); + }); + + describe('input domain does not have a default name set', () => { + it('returns undefined', async () => { + expect(await client.getName(nonExistingWalletAddress)).toBeUndefined(); + }); + }); + }); + + describe('getNameObject', () => { + it('returns related data of the name', async () => { + expect( + await client.getNameObject(domainName, { + showOwner: true, + showAvatar: true, + }), + ).toMatchObject({ + id: '0x7ee9ac31830e91f76f149952f7544b6d007b9a5520815e3d30264fa3d2791ad1', + nftId: '0x2879ff9464f06c0779ca34eec6138459a3e9855852dd5d1a025164c344b2b555', + expirationTimestampMs: '1715765005617', + owner: walletAddress, + targetAddress: walletAddress, + // avatar: 'https://api-testnet.iotafrens.iota.io/iotafrens/0x4e3ba002444df6c6774f41833f881d351533728d585343c58cca1fec1fef74ef/svg', + contentHash: 'QmZsHKQk9FbQZYCy7rMYn1z6m9Raa183dNhpGCRm3fX71s', + }); + }); + + it('Does not include avatar if the flag is off', async () => { + expect( + await client.getNameObject(domainName, { + showOwner: true, + }), + ).not.toHaveProperty('avatar'); + }); + + it('Does not include owner if the flag is off', async () => { + expect(await client.getNameObject(domainName)).not.toHaveProperty('owner'); + }); + }); +}); diff --git a/sdk/iotans-toolkit/tsconfig.esm.json b/sdk/iotans-toolkit/tsconfig.esm.json new file mode 100644 index 00000000000..df141cc11d4 --- /dev/null +++ b/sdk/iotans-toolkit/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "outDir": "dist/esm" + } +} diff --git a/sdk/iotans-toolkit/tsconfig.json b/sdk/iotans-toolkit/tsconfig.json new file mode 100644 index 00000000000..fff99c501c8 --- /dev/null +++ b/sdk/iotans-toolkit/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../build-scripts/tsconfig.shared.json", + "include": ["src"], + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist/cjs", + "isolatedModules": true, + "rootDir": "src" + }, + "references": [{ "path": "../typescript" }] +} diff --git a/sdk/iotans-toolkit/vitest.config.ts b/sdk/iotans-toolkit/vitest.config.ts new file mode 100644 index 00000000000..7d80da00a9c --- /dev/null +++ b/sdk/iotans-toolkit/vitest.config.ts @@ -0,0 +1,24 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { config } from 'dotenv'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + minThreads: 1, + maxThreads: 8, + hookTimeout: 1000000, + testTimeout: 1000000, + env: { + ...config({ path: '../.env.defaults' }).parsed, + }, + }, + resolve: { + alias: { + '@iota/bcs': new URL('../bcs/src', import.meta.url).toString(), + '@iota/iota-sdk': new URL('../typescript/src', import.meta.url).toString(), + }, + }, +}); diff --git a/sdk/typescript/src/client/client.ts b/sdk/typescript/src/client/client.ts index 0ae97a94211..f2800aa5389 100644 --- a/sdk/typescript/src/client/client.ts +++ b/sdk/typescript/src/client/client.ts @@ -69,6 +69,7 @@ import type { ProtocolConfig, QueryEventsParams, QueryTransactionBlocksParams, + ResolvedNameServiceNames, SubscribeEventParams, SubscribeTransactionParams, IotaEvent, @@ -788,6 +789,18 @@ export class IotaClient { return toHEX(bytes.slice(0, 4)); } + async resolveNameServiceAddress(_input: any): Promise<string | null> { + return 'remove_me'; + } + + async resolveNameServiceNames(_input: any): Promise<ResolvedNameServiceNames> { + return { + data: [], + hasNextPage: false, + nextCursor: null, + }; + } + async getProtocolConfig(input?: GetProtocolConfigParams): Promise<ProtocolConfig> { return await this.transport.request({ method: 'iota_getProtocolConfig', diff --git a/sdk/typescript/src/client/types/chain.ts b/sdk/typescript/src/client/types/chain.ts index 79e96127eb7..8124c20121c 100644 --- a/sdk/typescript/src/client/types/chain.ts +++ b/sdk/typescript/src/client/types/chain.ts @@ -14,6 +14,12 @@ import type { IotaTransaction, } from './generated.js'; +export type ResolvedNameServiceNames = { + data: string[]; + hasNextPage: boolean; + nextCursor: string | null; +}; + export type EpochPage = { data: EpochInfo[]; nextCursor: string | null;