From a52870448a5618ec3f6354aa77dd6958f9504270 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Tue, 13 Feb 2024 19:38:32 +0100 Subject: [PATCH 01/11] feat: add output tab to address page --- .../nova/address/AccountAddressView.tsx | 26 +++++--- .../nova/address/AnchorAddressView.tsx | 27 +++++--- .../nova/address/Ed25519AddressView.tsx | 28 +++++--- .../ImplicitAccountCreationAddressView.tsx | 28 +++++--- .../nova/address/NftAddressView.tsx | 27 +++++--- .../section/AddressPageTabbedSections.tsx | 66 +++++++++++++++++++ 6 files changed, 159 insertions(+), 43 deletions(-) create mode 100644 client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx diff --git a/client/src/app/components/nova/address/AccountAddressView.tsx b/client/src/app/components/nova/address/AccountAddressView.tsx index 9ef70952f..043838f47 100644 --- a/client/src/app/components/nova/address/AccountAddressView.tsx +++ b/client/src/app/components/nova/address/AccountAddressView.tsx @@ -1,17 +1,26 @@ import { AccountAddress } from "@iota/sdk-wasm-nova/web"; -import React from "react"; +import React, { Reducer, useReducer } from "react"; import { useAccountAddressState } from "~/helpers/nova/hooks/useAccountAddressState"; import Spinner from "../../Spinner"; import Bech32Address from "../../nova/address/Bech32Address"; -import AssociatedOutputs from "./section/association/AssociatedOutputs"; +import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; interface AccountAddressViewProps { accountAddress: AccountAddress; } +export interface IAccountAddressViewState { + isAssociatedOutputsLoading: boolean; +} + const AccountAddressView: React.FC = ({ accountAddress }) => { const { accountAddressDetails, isAccountDetailsLoading } = useAccountAddressState(accountAddress); - const isPageLoading = isAccountDetailsLoading; + const [state, setState] = useReducer>>( + (currentState, newState) => ({ ...currentState, ...newState }), + { isAssociatedOutputsLoading: false }, + ); + const { isAssociatedOutputsLoading } = state; + const isPageLoading = isAccountDetailsLoading || isAssociatedOutputsLoading; return (
@@ -36,12 +45,11 @@ const AccountAddressView: React.FC = ({ accountAddress
-
-
-

Associated Outputs

-
- -
+ setState({ isAssociatedOutputsLoading: val })} + /> )} diff --git a/client/src/app/components/nova/address/AnchorAddressView.tsx b/client/src/app/components/nova/address/AnchorAddressView.tsx index 6a0b2b161..a9d9a191c 100644 --- a/client/src/app/components/nova/address/AnchorAddressView.tsx +++ b/client/src/app/components/nova/address/AnchorAddressView.tsx @@ -1,17 +1,27 @@ import { AnchorAddress } from "@iota/sdk-wasm-nova/web"; -import React from "react"; +import React, { Reducer, useReducer } from "react"; import { useAnchorAddressState } from "~/helpers/nova/hooks/useAnchorAddressState"; import Spinner from "../../Spinner"; import Bech32Address from "./Bech32Address"; -import AssociatedOutputs from "./section/association/AssociatedOutputs"; +import { IEd25519AddressViewState } from "./Ed25519AddressView"; +import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; interface AnchorAddressViewProps { anchorAddress: AnchorAddress; } +export interface IAnchorAddressViewState { + isAssociatedOutputsLoading: boolean; +} + const AnchorAddressView: React.FC = ({ anchorAddress }) => { const { anchorAddressDetails, isAnchorDetailsLoading } = useAnchorAddressState(anchorAddress); - const isPageLoading = isAnchorDetailsLoading; + const [state, setState] = useReducer>>( + (currentState, newState) => ({ ...currentState, ...newState }), + { isAssociatedOutputsLoading: false }, + ); + const { isAssociatedOutputsLoading } = state; + const isPageLoading = isAnchorDetailsLoading || isAssociatedOutputsLoading; return (
@@ -36,12 +46,11 @@ const AnchorAddressView: React.FC = ({ anchorAddress })
-
-
-

Associated Outputs

-
- -
+ setState({ isAssociatedOutputsLoading: val })} + /> )} diff --git a/client/src/app/components/nova/address/Ed25519AddressView.tsx b/client/src/app/components/nova/address/Ed25519AddressView.tsx index 17232782d..322c46ff0 100644 --- a/client/src/app/components/nova/address/Ed25519AddressView.tsx +++ b/client/src/app/components/nova/address/Ed25519AddressView.tsx @@ -1,15 +1,27 @@ import { Ed25519Address } from "@iota/sdk-wasm-nova/web"; -import React from "react"; +import React, { Reducer, useReducer } from "react"; import { useEd25519AddressState } from "~/helpers/nova/hooks/useEd25519AddressState"; import Bech32Address from "./Bech32Address"; -import AssociatedOutputs from "./section/association/AssociatedOutputs"; +import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; +import Spinner from "../../Spinner"; interface Ed25519AddressViewProps { ed25519Address: Ed25519Address; } +export interface IEd25519AddressViewState { + isAssociatedOutputsLoading: boolean; +} + const Ed25519AddressView: React.FC = ({ ed25519Address }) => { const { ed25519AddressDetails } = useEd25519AddressState(ed25519Address); + const [state, setState] = useReducer>>( + (currentState, newState) => ({ ...currentState, ...newState }), + { isAssociatedOutputsLoading: false }, + ); + + const { isAssociatedOutputsLoading } = state; + const isPageLoading = isAssociatedOutputsLoading; return (
@@ -19,6 +31,7 @@ const Ed25519AddressView: React.FC = ({ ed25519Address

{ed25519AddressDetails.label?.replace("Ed25519", "Address")}

+ {isPageLoading && }
@@ -33,12 +46,11 @@ const Ed25519AddressView: React.FC = ({ ed25519Address
-
-
-

Associated Outputs

-
- -
+ setState({ isAssociatedOutputsLoading: val })} + /> )} diff --git a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx index eee1977ba..2da1d094d 100644 --- a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx +++ b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx @@ -1,15 +1,27 @@ import { ImplicitAccountCreationAddress } from "@iota/sdk-wasm-nova/web"; -import React from "react"; +import React, { Reducer, useReducer } from "react"; import { useImplicitAccountCreationAddressState } from "~/helpers/nova/hooks/useImplicitAccountCreationAddressState"; import Bech32Address from "./Bech32Address"; -import AssociatedOutputs from "./section/association/AssociatedOutputs"; +import Spinner from "../../Spinner"; +import { IEd25519AddressViewState } from "./Ed25519AddressView"; +import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; interface ImplicitAccountCreationAddressViewProps { implicitAccountCreationAddress: ImplicitAccountCreationAddress; } +export interface IImplicitAccountCreationAddressViewState { + isAssociatedOutputsLoading: boolean; +} + const ImplicitAccountCreationAddressView: React.FC = ({ implicitAccountCreationAddress }) => { const { implicitAccountCreationAddressDetails } = useImplicitAccountCreationAddressState(implicitAccountCreationAddress); + const [state, setState] = useReducer>>( + (currentState, newState) => ({ ...currentState, ...newState }), + { isAssociatedOutputsLoading: false }, + ); + const { isAssociatedOutputsLoading } = state; + const isPageLoading = isAssociatedOutputsLoading; return (
@@ -20,6 +32,7 @@ const ImplicitAccountCreationAddressView: React.FC

{implicitAccountCreationAddressDetails.label?.replace("Ed25519", "Address")}

+ {isPageLoading && }
@@ -33,12 +46,11 @@ const ImplicitAccountCreationAddressView: React.FC
-
-
-

Associated Outputs

-
- -
+ setState({ isAssociatedOutputsLoading: val })} + /> )} diff --git a/client/src/app/components/nova/address/NftAddressView.tsx b/client/src/app/components/nova/address/NftAddressView.tsx index f898f124f..9df57dba9 100644 --- a/client/src/app/components/nova/address/NftAddressView.tsx +++ b/client/src/app/components/nova/address/NftAddressView.tsx @@ -1,17 +1,27 @@ import { NftAddress } from "@iota/sdk-wasm-nova/web"; -import React from "react"; +import React, { Reducer, useReducer } from "react"; import { useNftAddressState } from "~/helpers/nova/hooks/useNftAddressState"; import Spinner from "../../Spinner"; import Bech32Address from "./Bech32Address"; -import AssociatedOutputs from "./section/association/AssociatedOutputs"; +import { IEd25519AddressViewState } from "./Ed25519AddressView"; +import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; interface NftAddressViewProps { nftAddress: NftAddress; } +export interface INftAddressViewState { + isAssociatedOutputsLoading: boolean; +} + const NftAddressView: React.FC = ({ nftAddress }) => { const { nftAddressDetails, isNftDetailsLoading } = useNftAddressState(nftAddress); - const isPageLoading = isNftDetailsLoading; + const [state, setState] = useReducer>>( + (currentState, newState) => ({ ...currentState, ...newState }), + { isAssociatedOutputsLoading: false }, + ); + const { isAssociatedOutputsLoading } = state; + const isPageLoading = isNftDetailsLoading || isAssociatedOutputsLoading; return (
@@ -36,12 +46,11 @@ const NftAddressView: React.FC = ({ nftAddress }) => {
-
-
-

Associated Outputs

-
- -
+ setState({ isAssociatedOutputsLoading: val })} + /> )} diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx new file mode 100644 index 000000000..36acdbab3 --- /dev/null +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -0,0 +1,66 @@ +import { AddressType } from "@iota/sdk-wasm-nova/web"; +import React, { useState } from "react"; +import associatedOuputsMessage from "~assets/modals/stardust/address/associated-outputs.json"; +import TabbedSection from "../../../hoc/TabbedSection"; +import AssociatedOutputs from "./association/AssociatedOutputs"; +import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; + +enum DEFAULT_TABS { + AssocOutputs = "Outputs", +} + +const buildDefaultTabsOptions = (associatedOutputCount: number) => ({ + [DEFAULT_TABS.AssocOutputs]: { + disabled: associatedOutputCount === 0, + counter: associatedOutputCount, + infoContent: associatedOuputsMessage, + }, +}); + +interface IAddressPageTabbedSectionsProps { + readonly addressDetails: IAddressDetails; + readonly setAssociatedOutputsLoading: (isLoading: boolean) => void; +} + +export const AddressPageTabbedSections: React.FC = ({ addressDetails, setAssociatedOutputsLoading }) => { + const [outputCount, setOutputCount] = useState(0); + + if (!addressDetails) { + return null; + } + + const defaultSections = [ + , + ]; + + const tabEnums = DEFAULT_TABS; + const defaultTabsOptions = buildDefaultTabsOptions(outputCount); + const tabOptions = defaultTabsOptions; + const tabbedSections = defaultSections; + + switch (addressDetails.type) { + case AddressType.Account: { + break; + } + case AddressType.Nft: { + break; + } + case AddressType.Anchor: { + break; + } + default: { + break; + } + } + + return ( + + {tabbedSections} + + ); +}; From aed231321a78d72d1b68fdba4a7b8ebbd2401782 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Wed, 14 Feb 2024 15:18:54 +0100 Subject: [PATCH 02/11] fix: add components to display native tokens --- .../models/api/nova/nova/IFoundryRequest.ts | 11 ++ .../models/api/nova/nova/IFoundryResponse.ts | 11 ++ api/src/routes.ts | 1 + api/src/routes/nova/foundry/get.ts | 30 +++++ api/src/services/nova/novaApiService.ts | 21 ++++ .../section/AddressPageTabbedSections.tsx | 14 ++- .../address/section/native-tokens/Asset.tsx | 108 ++++++++++++++++++ .../section/native-tokens/AssetProps.tsx | 13 +++ .../section/native-tokens/AssetsTable.scss | 102 +++++++++++++++++ .../section/native-tokens/AssetsTable.tsx | 108 ++++++++++++++++++ .../helpers/nova/hooks/useFoundryDetails.ts | 48 ++++++++ .../api/nova/foundry/IFoundryRequest.ts | 11 ++ .../api/nova/foundry/IFoundryResponse.ts | 9 ++ client/src/services/nova/novaApiClient.ts | 11 ++ 14 files changed, 496 insertions(+), 2 deletions(-) create mode 100644 api/src/models/api/nova/nova/IFoundryRequest.ts create mode 100644 api/src/models/api/nova/nova/IFoundryResponse.ts create mode 100644 api/src/routes/nova/foundry/get.ts create mode 100644 client/src/app/components/nova/address/section/native-tokens/Asset.tsx create mode 100644 client/src/app/components/nova/address/section/native-tokens/AssetProps.tsx create mode 100644 client/src/app/components/nova/address/section/native-tokens/AssetsTable.scss create mode 100644 client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx create mode 100644 client/src/helpers/nova/hooks/useFoundryDetails.ts create mode 100644 client/src/models/api/nova/foundry/IFoundryRequest.ts create mode 100644 client/src/models/api/nova/foundry/IFoundryResponse.ts diff --git a/api/src/models/api/nova/nova/IFoundryRequest.ts b/api/src/models/api/nova/nova/IFoundryRequest.ts new file mode 100644 index 000000000..6c655435c --- /dev/null +++ b/api/src/models/api/nova/nova/IFoundryRequest.ts @@ -0,0 +1,11 @@ +export interface IFoundryRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The foundry id to get the foundry details for. + */ + foundryId: string; +} diff --git a/api/src/models/api/nova/nova/IFoundryResponse.ts b/api/src/models/api/nova/nova/IFoundryResponse.ts new file mode 100644 index 000000000..9bd84790d --- /dev/null +++ b/api/src/models/api/nova/nova/IFoundryResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { OutputResponse } from "@iota/sdk-nova"; +import { IResponse } from "../IResponse"; + +export interface IFoundryResponse extends IResponse { + /** + * The foundry details response. + */ + foundryDetails?: OutputResponse; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index d8b1ade11..b0e0bd2b0 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -207,6 +207,7 @@ export const routes: IRoute[] = [ { path: "/nova/account/:network/:accountId", method: "get", folder: "nova/account", func: "get" }, { path: "/nova/nft/:network/:nftId", method: "get", folder: "nova/nft", func: "get" }, { path: "/nova/anchor/:network/:anchorId", method: "get", folder: "nova/anchor", func: "get" }, + { path: "/nova/foundry/:network/:foundryId", method: "get", folder: "nova/foundry", func: "get" }, { path: "/nova/output/associated/:network/:address", method: "post", diff --git a/api/src/routes/nova/foundry/get.ts b/api/src/routes/nova/foundry/get.ts new file mode 100644 index 000000000..cd901fe82 --- /dev/null +++ b/api/src/routes/nova/foundry/get.ts @@ -0,0 +1,30 @@ +import { ServiceFactory } from "../../../factories/serviceFactory"; +import { IFoundryRequest } from "../../../models/api/nova/nova/IFoundryRequest"; +import { IFoundryResponse } from "../../../models/api/nova/nova/IFoundryResponse"; +import { IConfiguration } from "../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../models/db/protocolVersion"; +import { NetworkService } from "../../../services/networkService"; +import { NovaApiService } from "../../../services/nova/novaApiService"; +import { ValidationHelper } from "../../../utils/validationHelper"; + +/** + * Get foundry output details by Foundry id. + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: IFoundryRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.foundryId, "foundryId"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.foundryDetails(request.foundryId); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index b3388a8db..b2377c85e 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -10,6 +10,7 @@ import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse"; +import { IFoundryResponse } from "../../models/api/nova/nova/IFoundryResponse"; import { INetwork } from "../../models/db/INetwork"; import { HexHelper } from "../../utils/hexHelper"; @@ -144,6 +145,26 @@ export class NovaApiService { } } + /** + * Get the foundry details. + * @param foundryId The foundryId to get the details for. + * @returns The foundry details. + */ + public async foundryDetails(foundryId: string): Promise { + try { + const foundryOutputId = await this.client.foundryOutputId(foundryId); + + if (foundryOutputId) { + const outputResponse = await this.outputDetails(foundryOutputId); + + return outputResponse.error ? { error: outputResponse.error } : { foundryDetails: outputResponse.output }; + } + return { message: "Foundry output not found" }; + } catch { + return { message: "Foundry output not found" }; + } + } + /** * Get the output mana rewards. * @param outputId The outputId to get the rewards for. diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx index 36acdbab3..e2a954c0b 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -3,18 +3,26 @@ import React, { useState } from "react"; import associatedOuputsMessage from "~assets/modals/stardust/address/associated-outputs.json"; import TabbedSection from "../../../hoc/TabbedSection"; import AssociatedOutputs from "./association/AssociatedOutputs"; +import nativeTokensMessage from "~assets/modals/stardust/address/assets-in-wallet.json"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; +// import AssetsTable from "./native-tokens/AssetsTable"; enum DEFAULT_TABS { + NativeTokens = "Native Tokens", AssocOutputs = "Outputs", } -const buildDefaultTabsOptions = (associatedOutputCount: number) => ({ +const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: number) => ({ [DEFAULT_TABS.AssocOutputs]: { disabled: associatedOutputCount === 0, counter: associatedOutputCount, infoContent: associatedOuputsMessage, }, + [DEFAULT_TABS.NativeTokens]: { + disabled: tokensCount === 0, + counter: tokensCount, + infoContent: nativeTokensMessage, + }, }); interface IAddressPageTabbedSectionsProps { @@ -24,12 +32,14 @@ interface IAddressPageTabbedSectionsProps { export const AddressPageTabbedSections: React.FC = ({ addressDetails, setAssociatedOutputsLoading }) => { const [outputCount, setOutputCount] = useState(0); + const [tokensCount] = useState(0); if (!addressDetails) { return null; } const defaultSections = [ + // , = ({ tableFormat, token }) => { + const { name: network } = useNetworkInfoNova((s) => s.networkInfo); + const [foundryDetails, isLoading] = useFoundryDetails(network, token.id); + const [tokenMetadata, setTokenMetadata] = useState(null); + const [isWhitelisted] = useTokenRegistryNativeTokenCheck(token.id); + + useEffect(() => { + if (isWhitelisted && foundryDetails) { + const immutableFeatures = (foundryDetails?.output as unknown as FoundryOutput).immutableFeatures; + + const metadata = immutableFeatures?.find((feature) => feature.type === FeatureType.Metadata) as MetadataFeature; + + if (metadata) { + updateTokenInfo(metadata); + } + } + }, [isWhitelisted, foundryDetails]); + + const updateTokenInfo = (metadata: MetadataFeature): void => { + const validator = new JsonSchemaValidator(); + + try { + const tokenInfo = JSON.parse(Converter.hexToUtf8(metadata.data)) as Irc30Metadata; + const result = validator.validate(tokenInfo, tokenSchemeIRC30); + + if (result.valid) { + setTokenMetadata(tokenInfo); + } + } catch {} + }; + + const buildTokenName = (name: string, logoUrl?: string): string | ReactElement => { + if (logoUrl) { + return ( + + {name} + {name} + + ); + } + return name; + }; + + /** + * Render the component. + * @returns The node to render. + */ + return tableFormat ? ( + + + {isLoading ? : tokenMetadata?.name ? buildTokenName(tokenMetadata.name, tokenMetadata.logoUrl) : "-"} + + {isLoading ? : tokenMetadata?.symbol ?? "-"} + + + + {token.amount.toString() ?? "-"} + + ) : ( +
+
+
Name
+
+ {isLoading ? ( + + ) : tokenMetadata?.name ? ( + buildTokenName(tokenMetadata.name, tokenMetadata.logoUrl) + ) : ( + "-" + )} +
+
+
+
Symbol
+
{isLoading ? : tokenMetadata?.symbol ?? "-"}
+
+
+
Token id
+
+ +
+
+
+
Quantity
+
{token.amount.toString() ?? "-"}
+
+
+ ); +}; + +export default Asset; diff --git a/client/src/app/components/nova/address/section/native-tokens/AssetProps.tsx b/client/src/app/components/nova/address/section/native-tokens/AssetProps.tsx new file mode 100644 index 000000000..5d1dc8511 --- /dev/null +++ b/client/src/app/components/nova/address/section/native-tokens/AssetProps.tsx @@ -0,0 +1,13 @@ +import { IToken } from "~models/api/stardust/foundry/IToken"; + +export interface AssetProps { + /** + * Token + */ + token: IToken; + + /** + * True if the asset is rendered like a table + */ + tableFormat?: boolean; +} diff --git a/client/src/app/components/nova/address/section/native-tokens/AssetsTable.scss b/client/src/app/components/nova/address/section/native-tokens/AssetsTable.scss new file mode 100644 index 000000000..f203b9871 --- /dev/null +++ b/client/src/app/components/nova/address/section/native-tokens/AssetsTable.scss @@ -0,0 +1,102 @@ +@import "../../../../../../scss/fonts"; +@import "../../../../../../scss/media-queries"; +@import "../../../../../../scss/mixins"; +@import "../../../../../../scss/variables"; +@import "../../../../../../scss/themes"; + +.asset-table { + width: 100%; + border-spacing: 12px 28px; + border-collapse: separate; + + @include tablet-down { + display: none; + } + + tr { + @include font-size(14px); + + color: $gray-7; + font-family: $inter; + letter-spacing: 0.5px; + + th { + @include font-size(12px); + + color: $gray-6; + font-weight: 600; + text-align: left; + text-transform: uppercase; + } + + td { + color: var(--body-color); + + &.highlight { + color: var(--link-color); + @include font-size(14px); + + font-family: $ibm-plex-mono; + font-weight: normal; + letter-spacing: 0.02em; + line-height: 20px; + + a { + max-width: 200px; + } + } + &.truncate { + max-width: 150px; + } + } + } +} + +.asset-cards { + display: none; + + @include tablet-down { + display: block; + + .asset-card { + margin-bottom: 48px; + + .field { + margin-bottom: 8px; + + .label { + color: $gray-6; + font-family: $inter; + letter-spacing: 0.5px; + + @include font-size(14px, 21px); + } + + .value { + @include font-size(14px, 21px); + + color: var(--body-color); + font-family: $metropolis; + font-weight: 700; + + .highlight { + color: var(--link-color); + @include font-size(14px); + + font-family: $ibm-plex-mono; + font-weight: normal; + letter-spacing: 0.02em; + line-height: 20px; + max-width: 200px; + } + } + } + } + } +} + +.token__logo { + vertical-align: middle; + height: 16px; + width: 16px; +} diff --git a/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx b/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx new file mode 100644 index 000000000..99e13dc7a --- /dev/null +++ b/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx @@ -0,0 +1,108 @@ +import { OutputType, OutputResponse, CommonOutput, INativeToken, FeatureType, NativeTokenFeature } from "@iota/sdk-wasm-nova/web"; +import React, { useEffect, useState } from "react"; +import Asset from "./Asset"; +import Pagination from "../../../../Pagination"; +import { plainToInstance } from "class-transformer"; +import "./AssetsTable.scss"; + +interface AssetsTableProps { + readonly outputs: OutputResponse[] | null; + readonly setTokensCount?: (count: number) => void; +} + +const TOKEN_PAGE_SIZE: number = 10; + +const AssetsTable: React.FC = ({ outputs, setTokensCount }) => { + const [tokens, setTokens] = useState(); + const [currentPage, setCurrentPage] = useState([]); + const [pageNumber, setPageNumber] = useState(1); + + useEffect(() => { + if (setTokensCount) { + setTokensCount(0); + } + + if (outputs) { + const theTokens: INativeToken[] = []; + for (const outputResponse of outputs) { + const output = outputResponse.output as CommonOutput; + if (output.type === OutputType.Basic || output.type === OutputType.Foundry) { + let nativeTokenFeature = output.features?.find((feature) => feature.type === FeatureType.NativeToken); + nativeTokenFeature = plainToInstance( + NativeTokenFeature, + nativeTokenFeature as NativeTokenFeature, + ) as unknown as NativeTokenFeature; + + if (nativeTokenFeature) { + const token = nativeTokenFeature as NativeTokenFeature; + const existingToken = theTokens.find((t) => t.id === token.asNativeToken().id); + // Convert to BigInt again in case the amount is hex + const amount = BigInt(token.amount); + if (existingToken) { + existingToken.amount += amount; + } else { + theTokens.push({ id: token.id, amount }); + } + } + } + } + + setTokens(theTokens); + if (setTokensCount) { + setTokensCount(theTokens.length); + } + } + }, [outputs]); + + useEffect(() => { + const from = (pageNumber - 1) * TOKEN_PAGE_SIZE; + const to = from + TOKEN_PAGE_SIZE; + if (tokens) { + setCurrentPage(tokens.slice(from, to)); + } + }, [tokens, pageNumber]); + + return tokens && tokens?.length > 0 ? ( +
+ + + + + + + + + + + {currentPage.map((token, k) => ( + + + + ))} + +
NameSymbolToken idQuantity
+ + {/* Only visible in mobile -- Card assets*/} +
+ {currentPage.map((token, k) => ( + + + + ))} +
+ setPageNumber(number)} + /> +
+ ) : null; +}; + +AssetsTable.defaultProps = { + setTokenCount: undefined, +}; + +export default AssetsTable; diff --git a/client/src/helpers/nova/hooks/useFoundryDetails.ts b/client/src/helpers/nova/hooks/useFoundryDetails.ts new file mode 100644 index 000000000..0e17bda35 --- /dev/null +++ b/client/src/helpers/nova/hooks/useFoundryDetails.ts @@ -0,0 +1,48 @@ +import { OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import { ServiceFactory } from "~factories/serviceFactory"; +import { NOVA } from "~models/config/protocolVersion"; +import { HexHelper } from "~/helpers/stardust/hexHelper"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; + +/** + * Fetch foundry output details + * @param network The Network in context + * @param foundryId The foundry id + * @returns The output response, loading bool and an error message. + */ +export function useFoundryDetails(network: string, foundryId: string | null): [OutputResponse | null, boolean, string?] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [foundryDetails, setFoundryDetails] = useState(null); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (foundryId) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .foundryDetails({ + network, + foundryId: HexHelper.addPrefix(foundryId), + }) + .then((response) => { + if (isMounted) { + setFoundryDetails(response.foundryDetails ?? null); + setError(response.error); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, foundryId]); + + return [foundryDetails, isLoading, error]; +} diff --git a/client/src/models/api/nova/foundry/IFoundryRequest.ts b/client/src/models/api/nova/foundry/IFoundryRequest.ts new file mode 100644 index 000000000..6c655435c --- /dev/null +++ b/client/src/models/api/nova/foundry/IFoundryRequest.ts @@ -0,0 +1,11 @@ +export interface IFoundryRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The foundry id to get the foundry details for. + */ + foundryId: string; +} diff --git a/client/src/models/api/nova/foundry/IFoundryResponse.ts b/client/src/models/api/nova/foundry/IFoundryResponse.ts new file mode 100644 index 000000000..6fed01eca --- /dev/null +++ b/client/src/models/api/nova/foundry/IFoundryResponse.ts @@ -0,0 +1,9 @@ +import { OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "../IResponse"; + +export interface IFoundryResponse extends IResponse { + /** + * The foundry details response. + */ + foundryDetails?: OutputResponse; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index 6635f8319..631e0a008 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -19,6 +19,8 @@ import { INftDetailsRequest } from "~/models/api/nova/INftDetailsRequest"; import { INftDetailsResponse } from "~/models/api/nova/INftDetailsResponse"; import { IAnchorDetailsRequest } from "~/models/api/nova/IAnchorDetailsRequest"; import { IAnchorDetailsResponse } from "~/models/api/nova/IAnchorDetailsResponse"; +import { IFoundryRequest } from "~/models/api/nova/foundry/IFoundryRequest"; +import { IFoundryResponse } from "~/models/api/nova/foundry/IFoundryResponse"; /** * Class to handle api communications on nova. @@ -87,6 +89,15 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/anchor/${request.network}/${request.anchorId}`, "get"); } + /** + * Get the foundry output details. + * @param request The request to send. + * @returns The response from the request. + */ + public async foundryDetails(request: IFoundryRequest): Promise { + return this.callApi(`nova/foundry/${request.network}/${request.foundryId}`, "get"); + } + /** * Get the associated outputs. * @param request The request to send. From 8ffe165656e6587def132da8b8c2711d01ca101a Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Wed, 14 Feb 2024 15:54:48 +0100 Subject: [PATCH 03/11] Remove unused code in AddressPageTabbedSections component --- .../section/AddressPageTabbedSections.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx index 36acdbab3..82ba54e4a 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -1,4 +1,3 @@ -import { AddressType } from "@iota/sdk-wasm-nova/web"; import React, { useState } from "react"; import associatedOuputsMessage from "~assets/modals/stardust/address/associated-outputs.json"; import TabbedSection from "../../../hoc/TabbedSection"; @@ -43,21 +42,6 @@ export const AddressPageTabbedSections: React.FC {tabbedSections} From 44fd78a9bc1e0949a7b991f8a8d072484f4971b6 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Wed, 14 Feb 2024 16:21:39 +0100 Subject: [PATCH 04/11] remove unused interfaces --- client/src/app/components/nova/address/AccountAddressView.tsx | 4 ---- client/src/app/components/nova/address/AnchorAddressView.tsx | 4 ---- client/src/app/components/nova/address/Ed25519AddressView.tsx | 4 ---- .../nova/address/ImplicitAccountCreationAddressView.tsx | 4 ---- client/src/app/components/nova/address/NftAddressView.tsx | 4 ---- 5 files changed, 20 deletions(-) diff --git a/client/src/app/components/nova/address/AccountAddressView.tsx b/client/src/app/components/nova/address/AccountAddressView.tsx index 42348213b..a818ab1ba 100644 --- a/client/src/app/components/nova/address/AccountAddressView.tsx +++ b/client/src/app/components/nova/address/AccountAddressView.tsx @@ -10,10 +10,6 @@ interface AccountAddressViewProps { accountAddress: AccountAddress; } -export interface IAccountAddressViewState { - isAssociatedOutputsLoading: boolean; -} - const AccountAddressView: React.FC = ({ accountAddress }) => { const [state, setState] = useAccountAddressState(accountAddress); const { accountAddressDetails, totalBalance, availableBalance, isAccountDetailsLoading, isAssociatedOutputsLoading } = state; diff --git a/client/src/app/components/nova/address/AnchorAddressView.tsx b/client/src/app/components/nova/address/AnchorAddressView.tsx index d9260af53..26133497d 100644 --- a/client/src/app/components/nova/address/AnchorAddressView.tsx +++ b/client/src/app/components/nova/address/AnchorAddressView.tsx @@ -10,10 +10,6 @@ interface AnchorAddressViewProps { anchorAddress: AnchorAddress; } -export interface IAnchorAddressViewState { - isAssociatedOutputsLoading: boolean; -} - const AnchorAddressView: React.FC = ({ anchorAddress }) => { const [state, setState] = useAnchorAddressState(anchorAddress); const { anchorAddressDetails, totalBalance, availableBalance, isAnchorDetailsLoading, isAssociatedOutputsLoading } = state; diff --git a/client/src/app/components/nova/address/Ed25519AddressView.tsx b/client/src/app/components/nova/address/Ed25519AddressView.tsx index 4a17fbca4..937111802 100644 --- a/client/src/app/components/nova/address/Ed25519AddressView.tsx +++ b/client/src/app/components/nova/address/Ed25519AddressView.tsx @@ -10,10 +10,6 @@ interface Ed25519AddressViewProps { ed25519Address: Ed25519Address; } -export interface IEd25519AddressViewState { - isAssociatedOutputsLoading: boolean; -} - const Ed25519AddressView: React.FC = ({ ed25519Address }) => { const [state, setState] = useEd25519AddressState(ed25519Address); const { ed25519AddressDetails, totalBalance, availableBalance, isAssociatedOutputsLoading } = state; diff --git a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx index c874af2cc..03e25303c 100644 --- a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx +++ b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx @@ -10,10 +10,6 @@ interface ImplicitAccountCreationAddressViewProps { implicitAccountCreationAddress: ImplicitAccountCreationAddress; } -export interface IImplicitAccountCreationAddressViewState { - isAssociatedOutputsLoading: boolean; -} - const ImplicitAccountCreationAddressView: React.FC = ({ implicitAccountCreationAddress }) => { const [state, setState] = useImplicitAccountCreationAddressState(implicitAccountCreationAddress); const { implicitAccountCreationAddressDetails, totalBalance, availableBalance, isAssociatedOutputsLoading } = state; diff --git a/client/src/app/components/nova/address/NftAddressView.tsx b/client/src/app/components/nova/address/NftAddressView.tsx index 143351284..a13f2d3c1 100644 --- a/client/src/app/components/nova/address/NftAddressView.tsx +++ b/client/src/app/components/nova/address/NftAddressView.tsx @@ -10,10 +10,6 @@ interface NftAddressViewProps { nftAddress: NftAddress; } -export interface INftAddressViewState { - isAssociatedOutputsLoading: boolean; -} - const NftAddressView: React.FC = ({ nftAddress }) => { const [state, setState] = useNftAddressState(nftAddress); const { nftAddressDetails, totalBalance, availableBalance, isNftDetailsLoading, isAssociatedOutputsLoading } = state; From 35d1cbb0d34f65bc954098badcf13443247cdc29 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Thu, 15 Feb 2024 09:45:01 +0100 Subject: [PATCH 05/11] Update address states and Add basic outputs API endpoint --- .../models/api/nova/IAddressDetailsRequest.ts | 11 ++++ .../api/nova/IAddressDetailsResponse.ts | 11 ++++ .../nova/{nova => foundry}/IFoundryRequest.ts | 0 .../{nova => foundry}/IFoundryResponse.ts | 0 api/src/routes.ts | 6 ++ .../routes/nova/address/outputs/basic/get.ts | 29 +++++++++ api/src/routes/nova/foundry/get.ts | 4 +- api/src/services/nova/novaApiService.ts | 61 ++++++++++++++++++- .../nova/address/AccountAddressView.tsx | 12 ++-- .../nova/address/AnchorAddressView.tsx | 12 ++-- .../nova/address/Ed25519AddressView.tsx | 14 ++--- .../ImplicitAccountCreationAddressView.tsx | 12 ++-- .../nova/address/NftAddressView.tsx | 12 ++-- .../section/AddressPageTabbedSections.tsx | 26 +++++--- .../nova/hooks/useAccountAddressState.ts | 20 ++++-- .../nova/hooks/useAddressBasicOutputs.ts | 43 +++++++++++++ .../nova/hooks/useAnchorAddressState.ts | 20 ++++-- .../nova/hooks/useEd25519AddressState.ts | 20 ++++-- .../useImplicitAccountCreationAddressState.ts | 20 ++++-- .../helpers/nova/hooks/useNftAddressState.ts | 20 ++++-- .../nova/address/IAddressDetailsRequest.ts | 11 ++++ .../nova/address/IAddressDetailsResponse.ts | 9 +++ client/src/services/nova/novaApiClient.ts | 11 ++++ 23 files changed, 311 insertions(+), 73 deletions(-) create mode 100644 api/src/models/api/nova/IAddressDetailsRequest.ts create mode 100644 api/src/models/api/nova/IAddressDetailsResponse.ts rename api/src/models/api/nova/{nova => foundry}/IFoundryRequest.ts (100%) rename api/src/models/api/nova/{nova => foundry}/IFoundryResponse.ts (100%) create mode 100644 api/src/routes/nova/address/outputs/basic/get.ts create mode 100644 client/src/helpers/nova/hooks/useAddressBasicOutputs.ts create mode 100644 client/src/models/api/nova/address/IAddressDetailsRequest.ts create mode 100644 client/src/models/api/nova/address/IAddressDetailsResponse.ts diff --git a/api/src/models/api/nova/IAddressDetailsRequest.ts b/api/src/models/api/nova/IAddressDetailsRequest.ts new file mode 100644 index 000000000..bddf1e371 --- /dev/null +++ b/api/src/models/api/nova/IAddressDetailsRequest.ts @@ -0,0 +1,11 @@ +export interface IAddressDetailsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The bech32 address to get the basic output ids for. + */ + address: string; +} diff --git a/api/src/models/api/nova/IAddressDetailsResponse.ts b/api/src/models/api/nova/IAddressDetailsResponse.ts new file mode 100644 index 000000000..56329c303 --- /dev/null +++ b/api/src/models/api/nova/IAddressDetailsResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { OutputResponse } from "@iota/sdk-nova"; +import { IResponse } from "./IResponse"; + +export interface IAddressDetailsResponse extends IResponse { + /** + * The outputs data. + */ + outputs?: OutputResponse[]; +} diff --git a/api/src/models/api/nova/nova/IFoundryRequest.ts b/api/src/models/api/nova/foundry/IFoundryRequest.ts similarity index 100% rename from api/src/models/api/nova/nova/IFoundryRequest.ts rename to api/src/models/api/nova/foundry/IFoundryRequest.ts diff --git a/api/src/models/api/nova/nova/IFoundryResponse.ts b/api/src/models/api/nova/foundry/IFoundryResponse.ts similarity index 100% rename from api/src/models/api/nova/nova/IFoundryResponse.ts rename to api/src/models/api/nova/foundry/IFoundryResponse.ts diff --git a/api/src/routes.ts b/api/src/routes.ts index 29197b7f8..18a36cf33 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -215,6 +215,12 @@ export const routes: IRoute[] = [ { path: "/nova/nft/:network/:nftId", method: "get", folder: "nova/nft", func: "get" }, { path: "/nova/anchor/:network/:anchorId", method: "get", folder: "nova/anchor", func: "get" }, { path: "/nova/foundry/:network/:foundryId", method: "get", folder: "nova/foundry", func: "get" }, + { + path: "/nova/address/outputs/basic/:network/:address", + method: "get", + folder: "nova/address/outputs/basic", + func: "get", + }, { path: "/nova/output/associated/:network/:address", method: "post", diff --git a/api/src/routes/nova/address/outputs/basic/get.ts b/api/src/routes/nova/address/outputs/basic/get.ts new file mode 100644 index 000000000..5daa98c6c --- /dev/null +++ b/api/src/routes/nova/address/outputs/basic/get.ts @@ -0,0 +1,29 @@ +import { ServiceFactory } from "../../../../../factories/serviceFactory"; +import { IAddressDetailsRequest } from "../../../../../models/api/nova/IAddressDetailsRequest"; +import { IAddressDetailsResponse } from "../../../../../models/api/nova/IAddressDetailsResponse"; +import { IConfiguration } from "../../../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../../../models/db/protocolVersion"; +import { NetworkService } from "../../../../../services/networkService"; +import { NovaApiService } from "../../../../../services/nova/novaApiService"; +import { ValidationHelper } from "../../../../../utils/validationHelper"; + +/** + * Fetch the basic output details by address. + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: IAddressDetailsRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.basicOutputDetailsByAddress(request.address); +} diff --git a/api/src/routes/nova/foundry/get.ts b/api/src/routes/nova/foundry/get.ts index cd901fe82..e27384a49 100644 --- a/api/src/routes/nova/foundry/get.ts +++ b/api/src/routes/nova/foundry/get.ts @@ -1,6 +1,6 @@ import { ServiceFactory } from "../../../factories/serviceFactory"; -import { IFoundryRequest } from "../../../models/api/nova/nova/IFoundryRequest"; -import { IFoundryResponse } from "../../../models/api/nova/nova/IFoundryResponse"; +import { IFoundryRequest } from "../../../models/api/nova/foundry/IFoundryRequest"; +import { IFoundryResponse } from "../../../models/api/nova/foundry/IFoundryResponse"; import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { NOVA } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 15eb25f19..e27b5773f 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -1,9 +1,11 @@ /* eslint-disable import/no-unresolved */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { Client } from "@iota/sdk-nova"; +import { Client, OutputResponse } from "@iota/sdk-nova"; import { ServiceFactory } from "../../factories/serviceFactory"; import logger from "../../logger"; +import { IFoundryResponse } from "../../models/api/nova/foundry/IFoundryResponse"; import { IAccountDetailsResponse } from "../../models/api/nova/IAccountDetailsResponse"; +import { IAddressDetailsResponse } from "../../models/api/nova/IAddressDetailsResponse"; import { IAnchorDetailsResponse } from "../../models/api/nova/IAnchorDetailsResponse"; import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse"; import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; @@ -11,7 +13,6 @@ import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse"; import { ISearchResponse } from "../../models/api/nova/ISearchResponse"; -import { IFoundryResponse } from "../../models/api/nova/nova/IFoundryResponse"; import { INetwork } from "../../models/db/INetwork"; import { HexHelper } from "../../utils/hexHelper"; import { SearchExecutor } from "../../utils/nova/searchExecutor"; @@ -168,12 +169,66 @@ export class NovaApiService { return outputResponse.error ? { error: outputResponse.error } : { foundryDetails: outputResponse.output }; } - return { message: "Foundry output not found" }; } catch { return { message: "Foundry output not found" }; } } + /** + * Get the outputs details. + * @param outputIds The output ids to get the details. + * @returns The item details. + */ + public async outputsDetails(outputIds: string[]): Promise { + const promises: Promise[] = []; + const outputResponses: OutputResponse[] = []; + + for (const outputId of outputIds) { + const promise = this.outputDetails(outputId); + promises.push(promise); + } + try { + await Promise.all(promises).then((results) => { + for (const outputDetails of results) { + if (outputDetails.output?.output && outputDetails.output?.metadata) { + outputResponses.push(outputDetails.output); + } + } + }); + + return outputResponses; + } catch (e) { + logger.error(`Fetching outputs details failed. Cause: ${e}`); + } + } + + /** + * Get the relevant basic output details for an address. + * @param addressBech32 The address in bech32 format. + * @returns The basic output details. + */ + public async basicOutputDetailsByAddress(addressBech32: string): Promise { + let cursor: string | undefined; + let outputIds: string[] = []; + + do { + try { + const outputIdsResponse = await this.client.basicOutputIds({ address: addressBech32, cursor: cursor ?? "" }); + + outputIds = outputIds.concat(outputIdsResponse.items); + cursor = outputIdsResponse.cursor; + } catch (e) { + logger.error(`Fetching basic output ids failed. Cause: ${e}`); + } + } while (cursor); + + const outputResponses = await this.outputsDetails(outputIds); + + return { + outputs: outputResponses, + }; + } + /** * Get the output mana rewards. * @param outputId The outputId to get the rewards for. diff --git a/client/src/app/components/nova/address/AccountAddressView.tsx b/client/src/app/components/nova/address/AccountAddressView.tsx index a818ab1ba..248b7c109 100644 --- a/client/src/app/components/nova/address/AccountAddressView.tsx +++ b/client/src/app/components/nova/address/AccountAddressView.tsx @@ -12,17 +12,17 @@ interface AccountAddressViewProps { const AccountAddressView: React.FC = ({ accountAddress }) => { const [state, setState] = useAccountAddressState(accountAddress); - const { accountAddressDetails, totalBalance, availableBalance, isAccountDetailsLoading, isAssociatedOutputsLoading } = state; + const { addressDetails, totalBalance, availableBalance, isAccountDetailsLoading, isAssociatedOutputsLoading } = state; const isPageLoading = isAccountDetailsLoading || isAssociatedOutputsLoading; return (
- {accountAddressDetails && ( + {addressDetails && (
-

{accountAddressDetails.label?.replace("Ed25519", "Address")}

+

{addressDetails.label?.replace("Ed25519", "Address")}

{isPageLoading && }
@@ -34,7 +34,7 @@ const AccountAddressView: React.FC = ({ accountAddress
- + {totalBalance !== null && ( = ({ accountAddress
setState({ isAssociatedOutputsLoading: val })} />
diff --git a/client/src/app/components/nova/address/AnchorAddressView.tsx b/client/src/app/components/nova/address/AnchorAddressView.tsx index 26133497d..3c30352f7 100644 --- a/client/src/app/components/nova/address/AnchorAddressView.tsx +++ b/client/src/app/components/nova/address/AnchorAddressView.tsx @@ -12,17 +12,17 @@ interface AnchorAddressViewProps { const AnchorAddressView: React.FC = ({ anchorAddress }) => { const [state, setState] = useAnchorAddressState(anchorAddress); - const { anchorAddressDetails, totalBalance, availableBalance, isAnchorDetailsLoading, isAssociatedOutputsLoading } = state; + const { addressDetails, totalBalance, availableBalance, isAnchorDetailsLoading, isAssociatedOutputsLoading } = state; const isPageLoading = isAnchorDetailsLoading || isAssociatedOutputsLoading; return (
- {anchorAddressDetails && ( + {addressDetails && (
-

{anchorAddressDetails.label?.replace("Ed25519", "Address")}

+

{addressDetails.label?.replace("Ed25519", "Address")}

{isPageLoading && }
@@ -34,7 +34,7 @@ const AnchorAddressView: React.FC = ({ anchorAddress })
- + {totalBalance !== null && ( = ({ anchorAddress })
setState({ isAssociatedOutputsLoading: val })} />
diff --git a/client/src/app/components/nova/address/Ed25519AddressView.tsx b/client/src/app/components/nova/address/Ed25519AddressView.tsx index 937111802..6c89a5717 100644 --- a/client/src/app/components/nova/address/Ed25519AddressView.tsx +++ b/client/src/app/components/nova/address/Ed25519AddressView.tsx @@ -12,17 +12,17 @@ interface Ed25519AddressViewProps { const Ed25519AddressView: React.FC = ({ ed25519Address }) => { const [state, setState] = useEd25519AddressState(ed25519Address); - const { ed25519AddressDetails, totalBalance, availableBalance, isAssociatedOutputsLoading } = state; - const isPageLoading = isAssociatedOutputsLoading; + const { addressDetails, totalBalance, availableBalance, isAssociatedOutputsLoading, isBasicOutputsLoading } = state; + const isPageLoading = isAssociatedOutputsLoading || isBasicOutputsLoading; return (
- {ed25519AddressDetails && ( + {addressDetails && (
-

{ed25519AddressDetails.label?.replace("Ed25519", "Address")}

+

{addressDetails.label?.replace("Ed25519", "Address")}

{isPageLoading && }
@@ -34,7 +34,7 @@ const Ed25519AddressView: React.FC = ({ ed25519Address
- + {totalBalance !== null && ( = ({ ed25519Address
setState({ isAssociatedOutputsLoading: val })} />
diff --git a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx index 03e25303c..05e0256ab 100644 --- a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx +++ b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx @@ -12,17 +12,17 @@ interface ImplicitAccountCreationAddressViewProps { const ImplicitAccountCreationAddressView: React.FC = ({ implicitAccountCreationAddress }) => { const [state, setState] = useImplicitAccountCreationAddressState(implicitAccountCreationAddress); - const { implicitAccountCreationAddressDetails, totalBalance, availableBalance, isAssociatedOutputsLoading } = state; + const { addressDetails, totalBalance, availableBalance, isAssociatedOutputsLoading } = state; const isPageLoading = isAssociatedOutputsLoading; return (
- {implicitAccountCreationAddressDetails && ( + {addressDetails && (
-

{implicitAccountCreationAddressDetails.label?.replace("Ed25519", "Address")}

+

{addressDetails.label?.replace("Ed25519", "Address")}

{isPageLoading && }
@@ -34,7 +34,7 @@ const ImplicitAccountCreationAddressView: React.FC
- + {totalBalance !== null && (
setState({ isAssociatedOutputsLoading: val })} />
diff --git a/client/src/app/components/nova/address/NftAddressView.tsx b/client/src/app/components/nova/address/NftAddressView.tsx index a13f2d3c1..51431739e 100644 --- a/client/src/app/components/nova/address/NftAddressView.tsx +++ b/client/src/app/components/nova/address/NftAddressView.tsx @@ -12,17 +12,17 @@ interface NftAddressViewProps { const NftAddressView: React.FC = ({ nftAddress }) => { const [state, setState] = useNftAddressState(nftAddress); - const { nftAddressDetails, totalBalance, availableBalance, isNftDetailsLoading, isAssociatedOutputsLoading } = state; + const { addressDetails, totalBalance, availableBalance, isNftDetailsLoading, isAssociatedOutputsLoading } = state; const isPageLoading = isNftDetailsLoading || isAssociatedOutputsLoading; return (
- {nftAddressDetails && ( + {addressDetails && (
-

{nftAddressDetails.label?.replace("Ed25519", "Address")}

+

{addressDetails.label?.replace("Ed25519", "Address")}

{isPageLoading && }
@@ -34,7 +34,7 @@ const NftAddressView: React.FC = ({ nftAddress }) => {
- + {totalBalance !== null && ( = ({ nftAddress }) => {
setState({ isAssociatedOutputsLoading: val })} />
diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx index 90c06f768..61e796402 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -3,8 +3,12 @@ import associatedOuputsMessage from "~assets/modals/stardust/address/associated- import TabbedSection from "../../../hoc/TabbedSection"; import AssociatedOutputs from "./association/AssociatedOutputs"; import nativeTokensMessage from "~assets/modals/stardust/address/assets-in-wallet.json"; -import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; -// import AssetsTable from "./native-tokens/AssetsTable"; +import { IAccountAddressState } from "~/helpers/nova/hooks/useAccountAddressState"; +import { INftAddressState } from "~/helpers/nova/hooks/useNftAddressState"; +import { IAnchorAddressState } from "~/helpers/nova/hooks/useAnchorAddressState"; +import { IEd25519AddressState } from "~/helpers/nova/hooks/useEd25519AddressState"; +import AssetsTable from "./native-tokens/AssetsTable"; +import { IImplicitAccountCreationAddressState } from "~/helpers/nova/hooks/useImplicitAccountCreationAddressState"; enum DEFAULT_TABS { NativeTokens = "Native Tokens", @@ -14,31 +18,39 @@ enum DEFAULT_TABS { const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: number) => ({ [DEFAULT_TABS.AssocOutputs]: { disabled: associatedOutputCount === 0, + hidden: associatedOutputCount === 0, counter: associatedOutputCount, infoContent: associatedOuputsMessage, }, [DEFAULT_TABS.NativeTokens]: { disabled: tokensCount === 0, + hidden: tokensCount === 0, counter: tokensCount, infoContent: nativeTokensMessage, }, }); interface IAddressPageTabbedSectionsProps { - readonly addressDetails: IAddressDetails; + readonly addressState: + | IEd25519AddressState + | IAccountAddressState + | INftAddressState + | IAnchorAddressState + | IImplicitAccountCreationAddressState; readonly setAssociatedOutputsLoading: (isLoading: boolean) => void; } -export const AddressPageTabbedSections: React.FC = ({ addressDetails, setAssociatedOutputsLoading }) => { +export const AddressPageTabbedSections: React.FC = ({ addressState, setAssociatedOutputsLoading }) => { const [outputCount, setOutputCount] = useState(0); - const [tokensCount] = useState(0); + const [tokensCount, setTokensCount] = useState(0); - if (!addressDetails) { + if (!addressState.addressDetails) { return null; } + const { addressDetails, addressBasicOutputs } = addressState; const defaultSections = [ - // , + , { const locationState = location.state as IAddressPageLocationProps; @@ -53,7 +59,7 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres setState({ ...initialState, - accountAddressDetails: addressDetails, + addressDetails, }); }, []); @@ -63,8 +69,10 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres isAccountDetailsLoading, totalBalance, availableBalance, + addressBasicOutputs, + isBasicOutputsLoading, }); - }, [accountOutput, totalBalance, availableBalance, isAccountDetailsLoading]); + }, [accountOutput, totalBalance, availableBalance, addressBasicOutputs, isAccountDetailsLoading, isBasicOutputsLoading]); return [state, setState]; }; diff --git a/client/src/helpers/nova/hooks/useAddressBasicOutputs.ts b/client/src/helpers/nova/hooks/useAddressBasicOutputs.ts new file mode 100644 index 000000000..a9e8fc920 --- /dev/null +++ b/client/src/helpers/nova/hooks/useAddressBasicOutputs.ts @@ -0,0 +1,43 @@ +import { OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import { ServiceFactory } from "~factories/serviceFactory"; +import { NOVA } from "~models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; + +/** + * Fetch Address basic UTXOs + * @param network The Network in context + * @param addressBech32 The address in bech32 format + * @returns The output responses and loading bool. + */ +export function useAddressBasicOutputs(network: string, addressBech32: string | null): [OutputResponse[] | null, boolean] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [outputs, setOutputs] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + setOutputs(null); + if (addressBech32) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .basicOutputsDetails({ network, address: addressBech32 }) + .then((response) => { + if (!response?.error && response.outputs && isMounted) { + setOutputs(response.outputs); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, addressBech32]); + + return [outputs, isLoading]; +} diff --git a/client/src/helpers/nova/hooks/useAnchorAddressState.ts b/client/src/helpers/nova/hooks/useAnchorAddressState.ts index b11062bce..a688fd186 100644 --- a/client/src/helpers/nova/hooks/useAnchorAddressState.ts +++ b/client/src/helpers/nova/hooks/useAnchorAddressState.ts @@ -1,5 +1,5 @@ import { Reducer, useEffect, useReducer } from "react"; -import { AnchorAddress, AnchorOutput } from "@iota/sdk-wasm-nova/web"; +import { AnchorAddress, AnchorOutput, OutputResponse } from "@iota/sdk-wasm-nova/web"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; import { useAnchorDetails } from "./useAnchorDetails"; import { useLocation, useParams } from "react-router-dom"; @@ -7,21 +7,26 @@ import { AddressRouteProps } from "~/app/routes/AddressRouteProps"; import { useNetworkInfoNova } from "../networkInfo"; import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; +import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; export interface IAnchorAddressState { - anchorAddressDetails: IAddressDetails | null; + addressDetails: IAddressDetails | null; anchorOutput: AnchorOutput | null; availableBalance: number | null; totalBalance: number | null; + addressBasicOutputs: OutputResponse[] | null; + isBasicOutputsLoading: boolean; isAnchorDetailsLoading: boolean; isAssociatedOutputsLoading: boolean; } const initialState = { - anchorAddressDetails: null, + addressDetails: null, anchorOutput: null, totalBalance: null, availableBalance: null, + addressBasicOutputs: null, + isBasicOutputsLoading: false, isAnchorDetailsLoading: true, isAssociatedOutputsLoading: false, }; @@ -43,7 +48,8 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt ); const { anchorOutput, isLoading: isAnchorDetailsLoading } = useAnchorDetails(network, address.anchorId); - const { totalBalance, availableBalance } = useAddressBalance(network, state.anchorAddressDetails, anchorOutput); + const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, anchorOutput); + const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -53,7 +59,7 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt setState({ ...initialState, - anchorAddressDetails: addressDetails, + addressDetails, }); }, []); @@ -62,9 +68,11 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt anchorOutput, totalBalance, availableBalance, + addressBasicOutputs, + isBasicOutputsLoading, isAnchorDetailsLoading, }); - }, [anchorOutput, totalBalance, availableBalance, isAnchorDetailsLoading]); + }, [anchorOutput, totalBalance, availableBalance, addressBasicOutputs, isBasicOutputsLoading, isAnchorDetailsLoading]); return [state, setState]; }; diff --git a/client/src/helpers/nova/hooks/useEd25519AddressState.ts b/client/src/helpers/nova/hooks/useEd25519AddressState.ts index d5cbffe8b..cc2866a50 100644 --- a/client/src/helpers/nova/hooks/useEd25519AddressState.ts +++ b/client/src/helpers/nova/hooks/useEd25519AddressState.ts @@ -1,22 +1,27 @@ -import { Ed25519Address } from "@iota/sdk-wasm-nova/web"; +import { Ed25519Address, OutputResponse } from "@iota/sdk-wasm-nova/web"; import { Reducer, useEffect, useReducer } from "react"; import { useLocation } from "react-router-dom"; import { useNetworkInfoNova } from "../networkInfo"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; +import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; export interface IEd25519AddressState { - ed25519AddressDetails: IAddressDetails | null; + addressDetails: IAddressDetails | null; totalBalance: number | null; availableBalance: number | null; + addressBasicOutputs: OutputResponse[] | null; + isBasicOutputsLoading: boolean; isAssociatedOutputsLoading: boolean; } const initialState = { - ed25519AddressDetails: null, + addressDetails: null, totalBalance: null, availableBalance: null, + addressBasicOutputs: null, + isBasicOutputsLoading: false, isAssociatedOutputsLoading: false, }; @@ -35,7 +40,8 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres initialState, ); - const { totalBalance, availableBalance } = useAddressBalance(network, state.ed25519AddressDetails, null); + const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, null); + const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -44,7 +50,7 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres : { addressDetails: AddressHelper.buildAddress(bech32Hrp, address) }; setState({ - ed25519AddressDetails: addressDetails, + addressDetails, }); }, []); @@ -52,8 +58,10 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres setState({ totalBalance, availableBalance, + addressBasicOutputs, + isBasicOutputsLoading, }); - }, [totalBalance, availableBalance]); + }, [totalBalance, availableBalance, addressBasicOutputs, isBasicOutputsLoading]); return [state, setState]; }; diff --git a/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts b/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts index e569462b6..d0277b00e 100644 --- a/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts +++ b/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts @@ -1,4 +1,4 @@ -import { ImplicitAccountCreationAddress } from "@iota/sdk-wasm-nova/web"; +import { ImplicitAccountCreationAddress, OutputResponse } from "@iota/sdk-wasm-nova/web"; import { Reducer, useEffect, useReducer } from "react"; import { useLocation, useParams } from "react-router-dom"; import { AddressRouteProps } from "~/app/routes/AddressRouteProps"; @@ -6,18 +6,23 @@ import { useNetworkInfoNova } from "../networkInfo"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; +import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; export interface IImplicitAccountCreationAddressState { - implicitAccountCreationAddressDetails: IAddressDetails | null; + addressDetails: IAddressDetails | null; totalBalance: number | null; availableBalance: number | null; + addressBasicOutputs: OutputResponse[] | null; + isBasicOutputsLoading: boolean; isAssociatedOutputsLoading: boolean; } const initialState = { - implicitAccountCreationAddressDetails: null, + addressDetails: null, totalBalance: null, availableBalance: null, + addressBasicOutputs: null, + isBasicOutputsLoading: false, isAssociatedOutputsLoading: false, }; @@ -39,7 +44,8 @@ export const useImplicitAccountCreationAddressState = ( initialState, ); - const { totalBalance, availableBalance } = useAddressBalance(network, state.implicitAccountCreationAddressDetails, null); + const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, null); + const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -49,7 +55,7 @@ export const useImplicitAccountCreationAddressState = ( setState({ ...initialState, - implicitAccountCreationAddressDetails: addressDetails, + addressDetails, }); }, []); @@ -57,8 +63,10 @@ export const useImplicitAccountCreationAddressState = ( setState({ totalBalance, availableBalance, + addressBasicOutputs, + isBasicOutputsLoading, }); - }, [totalBalance, availableBalance]); + }, [totalBalance, availableBalance, addressBasicOutputs, isBasicOutputsLoading]); return [state, setState]; }; diff --git a/client/src/helpers/nova/hooks/useNftAddressState.ts b/client/src/helpers/nova/hooks/useNftAddressState.ts index aa361ccca..ebc43c8dd 100644 --- a/client/src/helpers/nova/hooks/useNftAddressState.ts +++ b/client/src/helpers/nova/hooks/useNftAddressState.ts @@ -1,5 +1,5 @@ import { Reducer, useEffect, useReducer } from "react"; -import { NftAddress, NftOutput } from "@iota/sdk-wasm-nova/web"; +import { NftAddress, NftOutput, OutputResponse } from "@iota/sdk-wasm-nova/web"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; import { useNftDetails } from "./useNftDetails"; import { useLocation, useParams } from "react-router-dom"; @@ -7,22 +7,27 @@ import { AddressRouteProps } from "~/app/routes/AddressRouteProps"; import { useNetworkInfoNova } from "../networkInfo"; import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; +import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; export interface INftAddressState { - nftAddressDetails: IAddressDetails | null; + addressDetails: IAddressDetails | null; nftOutput: NftOutput | null; totalBalance: number | null; availableBalance: number | null; + addressBasicOutputs: OutputResponse[] | null; + isBasicOutputsLoading: boolean; isNftDetailsLoading: boolean; isAssociatedOutputsLoading: boolean; } const initialState = { - nftAddressDetails: null, + addressDetails: null, nftOutput: null, isNftDetailsLoading: true, totalBalance: null, availableBalance: null, + addressBasicOutputs: null, + isBasicOutputsLoading: false, isAssociatedOutputsLoading: false, }; @@ -43,7 +48,8 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac ); const { nftOutput, isLoading: isNftDetailsLoading } = useNftDetails(network, address.nftId); - const { totalBalance, availableBalance } = useAddressBalance(network, state.nftAddressDetails, nftOutput); + const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, nftOutput); + const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -53,7 +59,7 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac setState({ ...initialState, - nftAddressDetails: addressDetails, + addressDetails, }); }, []); @@ -63,8 +69,10 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac totalBalance, availableBalance, isNftDetailsLoading, + addressBasicOutputs, + isBasicOutputsLoading, }); - }, [nftOutput, totalBalance, availableBalance, isNftDetailsLoading]); + }, [nftOutput, totalBalance, availableBalance, isNftDetailsLoading, addressBasicOutputs, isBasicOutputsLoading]); return [state, setState]; }; diff --git a/client/src/models/api/nova/address/IAddressDetailsRequest.ts b/client/src/models/api/nova/address/IAddressDetailsRequest.ts new file mode 100644 index 000000000..0a8aa0b4f --- /dev/null +++ b/client/src/models/api/nova/address/IAddressDetailsRequest.ts @@ -0,0 +1,11 @@ +export interface IAddressDetailsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The bech32 address to get the unspent output ids for. + */ + address: string; +} diff --git a/client/src/models/api/nova/address/IAddressDetailsResponse.ts b/client/src/models/api/nova/address/IAddressDetailsResponse.ts new file mode 100644 index 000000000..5a075e38f --- /dev/null +++ b/client/src/models/api/nova/address/IAddressDetailsResponse.ts @@ -0,0 +1,9 @@ +import { OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "../IResponse"; + +export interface IAddressDetailsResponse extends IResponse { + /** + * The outputs data. + */ + outputs?: OutputResponse[]; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index ca2622851..ef66bffbe 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -25,6 +25,8 @@ import { IFoundryRequest } from "~/models/api/nova/foundry/IFoundryRequest"; import { IFoundryResponse } from "~/models/api/nova/foundry/IFoundryResponse"; import { ISearchRequest } from "~/models/api/nova/ISearchRequest"; import { ISearchResponse } from "~/models/api/nova/ISearchResponse"; +import { IAddressDetailsRequest } from "~/models/api/nova/address/IAddressDetailsRequest"; +import { IAddressDetailsResponse } from "~/models/api/nova/address/IAddressDetailsResponse"; /** * Class to handle api communications on nova. @@ -111,6 +113,15 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/foundry/${request.network}/${request.foundryId}`, "get"); } + /** + * Get the basic outputs details of an address. + * @param request The Address Basic outputs request. + * @returns The Address outputs response + */ + public async basicOutputsDetails(request: IAddressDetailsRequest): Promise { + return this.callApi(`nova/address/outputs/basic/${request.network}/${request.address}`, "get"); + } + /** * Get the associated outputs. * @param request The request to send. From 3a10fe5d644b5b96cc53dbeaa1c5c02eca5ea8ba Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Thu, 15 Feb 2024 10:02:49 +0100 Subject: [PATCH 06/11] Add eslint-disable for unsafe return in novaApiService.ts --- api/src/services/nova/novaApiService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index e27b5773f..7e3936e01 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -1,5 +1,6 @@ /* eslint-disable import/no-unresolved */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { Client, OutputResponse } from "@iota/sdk-nova"; import { ServiceFactory } from "../../factories/serviceFactory"; import logger from "../../logger"; From b390f0581b1281393ac105bedb0b9137e827a65f Mon Sep 17 00:00:00 2001 From: Bran <52735957+brancoder@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:37:57 +0100 Subject: [PATCH 07/11] Add Foundries tab to Accound address page (#1134) --- .../api/nova/foundry/IFoundriesRequest.ts | 11 +++ .../api/nova/foundry/IFoundriesResponse.ts | 11 +++ api/src/routes.ts | 6 ++ api/src/routes/nova/account/foundries/get.ts | 31 ++++++++ api/src/services/nova/novaApiService.ts | 20 ++++++ .../section/AddressPageTabbedSections.tsx | 49 ++++++++++++- .../account/AccountFoundriesSection.scss | 14 ++++ .../account/AccountFoundriesSection.tsx | 53 ++++++++++++++ .../nova/hooks/useAccountAddressState.ts | 8 +++ .../hooks/useAccountControlledFoundries.ts | 70 +++++++++++++++++++ .../api/nova/foundry/IFoundriesRequest.ts | 11 +++ .../api/nova/foundry/IFoundriesResponse.ts | 9 +++ client/src/services/nova/novaApiClient.ts | 11 +++ 13 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 api/src/models/api/nova/foundry/IFoundriesRequest.ts create mode 100644 api/src/models/api/nova/foundry/IFoundriesResponse.ts create mode 100644 api/src/routes/nova/account/foundries/get.ts create mode 100644 client/src/app/components/nova/address/section/account/AccountFoundriesSection.scss create mode 100644 client/src/app/components/nova/address/section/account/AccountFoundriesSection.tsx create mode 100644 client/src/helpers/nova/hooks/useAccountControlledFoundries.ts create mode 100644 client/src/models/api/nova/foundry/IFoundriesRequest.ts create mode 100644 client/src/models/api/nova/foundry/IFoundriesResponse.ts diff --git a/api/src/models/api/nova/foundry/IFoundriesRequest.ts b/api/src/models/api/nova/foundry/IFoundriesRequest.ts new file mode 100644 index 000000000..e19631692 --- /dev/null +++ b/api/src/models/api/nova/foundry/IFoundriesRequest.ts @@ -0,0 +1,11 @@ +export interface IFoundriesRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The bech32 account address to get the foundy output ids for. + */ + accountAddress: string; +} diff --git a/api/src/models/api/nova/foundry/IFoundriesResponse.ts b/api/src/models/api/nova/foundry/IFoundriesResponse.ts new file mode 100644 index 000000000..4f6db863b --- /dev/null +++ b/api/src/models/api/nova/foundry/IFoundriesResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { IOutputsResponse } from "@iota/sdk-nova"; +import { IResponse } from "../IResponse"; + +export interface IFoundriesResponse extends IResponse { + /** + * The output ids response. + */ + foundryOutputsResponse?: IOutputsResponse; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index 18a36cf33..c5f2edf37 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -228,6 +228,12 @@ export const routes: IRoute[] = [ func: "post", dataBody: true, }, + { + path: "/nova/account/foundries/:network/:accountAddress", + method: "get", + folder: "nova/account/foundries", + func: "get", + }, { path: "/nova/block/:network/:blockId", method: "get", folder: "nova/block", func: "get" }, { path: "/nova/block/metadata/:network/:blockId", method: "get", folder: "nova/block/metadata", func: "get" }, ]; diff --git a/api/src/routes/nova/account/foundries/get.ts b/api/src/routes/nova/account/foundries/get.ts new file mode 100644 index 000000000..77b1775c0 --- /dev/null +++ b/api/src/routes/nova/account/foundries/get.ts @@ -0,0 +1,31 @@ +import { ServiceFactory } from "../../../../factories/serviceFactory"; +import { IFoundriesRequest } from "../../../../models/api/nova/foundry/IFoundriesRequest"; +import { IFoundriesResponse } from "../../../../models/api/nova/foundry/IFoundriesResponse"; + +import { IConfiguration } from "../../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../../models/db/protocolVersion"; +import { NetworkService } from "../../../../services/networkService"; +import { NovaApiService } from "../../../../services/nova/novaApiService"; +import { ValidationHelper } from "../../../../utils/validationHelper"; + +/** + * Get controlled Foundry output id by controller Account address + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: IFoundriesRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.accountAddress, "aliasAddress"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.accountFoundries(request.accountAddress); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 7e3936e01..09d70ff27 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -4,6 +4,7 @@ import { Client, OutputResponse } from "@iota/sdk-nova"; import { ServiceFactory } from "../../factories/serviceFactory"; import logger from "../../logger"; +import { IFoundriesResponse } from "../../models/api/nova/foundry/IFoundriesResponse"; import { IFoundryResponse } from "../../models/api/nova/foundry/IFoundryResponse"; import { IAccountDetailsResponse } from "../../models/api/nova/IAccountDetailsResponse"; import { IAddressDetailsResponse } from "../../models/api/nova/IAddressDetailsResponse"; @@ -156,6 +157,25 @@ export class NovaApiService { } } + /** + * Get controlled Foundry output id by controller Account address + * @param accountAddress The bech32 account address to get the controlled Foundries for. + * @returns The foundry outputs. + */ + public async accountFoundries(accountAddress: string): Promise { + try { + const response = await this.client.foundryOutputIds({ account: accountAddress }); + + if (response) { + return { + foundryOutputsResponse: response, + }; + } + } catch { + return { message: "Foundries output not found" }; + } + } + /** * Get the foundry details. * @param foundryId The foundryId to get the details for. diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx index 61e796402..a57cf358e 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import associatedOuputsMessage from "~assets/modals/stardust/address/associated-outputs.json"; +import foundriesMessage from "~assets/modals/stardust/alias/foundries.json"; import TabbedSection from "../../../hoc/TabbedSection"; import AssociatedOutputs from "./association/AssociatedOutputs"; import nativeTokensMessage from "~assets/modals/stardust/address/assets-in-wallet.json"; @@ -9,12 +10,18 @@ import { IAnchorAddressState } from "~/helpers/nova/hooks/useAnchorAddressState" import { IEd25519AddressState } from "~/helpers/nova/hooks/useEd25519AddressState"; import AssetsTable from "./native-tokens/AssetsTable"; import { IImplicitAccountCreationAddressState } from "~/helpers/nova/hooks/useImplicitAccountCreationAddressState"; +import { AddressType } from "@iota/sdk-wasm-nova/web"; +import AccountFoundriesSection from "./account/AccountFoundriesSection"; enum DEFAULT_TABS { NativeTokens = "Native Tokens", AssocOutputs = "Outputs", } +enum ACCOUNT_TABS { + Foundries = "Foundries", +} + const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: number) => ({ [DEFAULT_TABS.AssocOutputs]: { disabled: associatedOutputCount === 0, @@ -30,6 +37,15 @@ const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: num }, }); +const buildAccountAddressTabsOptions = (foundriesCount: number, isAccountFoundriesLoading: boolean) => ({ + [ACCOUNT_TABS.Foundries]: { + disabled: foundriesCount === 0, + hidden: foundriesCount === 0, + isLoading: isAccountFoundriesLoading, + infoContent: foundriesMessage, + }, +}); + interface IAddressPageTabbedSectionsProps { readonly addressState: | IEd25519AddressState @@ -59,11 +75,38 @@ export const AddressPageTabbedSections: React.FC, ]; - const tabEnums = DEFAULT_TABS; + const accountAddressSections = + addressDetails.type === AddressType.Account + ? [ + , + ] + : null; + + let tabEnums = DEFAULT_TABS; const defaultTabsOptions = buildDefaultTabsOptions(tokensCount, outputCount); - const tabOptions = defaultTabsOptions; - const tabbedSections = defaultSections; + let tabOptions = defaultTabsOptions; + let tabbedSections = defaultSections; + switch (addressDetails.type) { + case AddressType.Account: { + tabEnums = { ...DEFAULT_TABS, ...ACCOUNT_TABS }; + tabOptions = { + ...defaultTabsOptions, + ...buildAccountAddressTabsOptions( + (addressState as IAccountAddressState).accountOutput?.foundryCounter ?? 0, + (addressState as IAccountAddressState).isFoundriesLoading, + ), + }; + tabbedSections = [...defaultSections, ...(accountAddressSections ?? [])]; + break; + } + default: { + break; + } + } return ( {tabbedSections} diff --git a/client/src/app/components/nova/address/section/account/AccountFoundriesSection.scss b/client/src/app/components/nova/address/section/account/AccountFoundriesSection.scss new file mode 100644 index 000000000..61a93ab8f --- /dev/null +++ b/client/src/app/components/nova/address/section/account/AccountFoundriesSection.scss @@ -0,0 +1,14 @@ +@import "../../../../../../scss/mixins.scss"; + +.controlled-foundry--card { + border: none !important; + margin-bottom: 48px; + + .field { + margin-bottom: 8px; + + .card--label { + @include font-size(14px, 21px); + } + } +} diff --git a/client/src/app/components/nova/address/section/account/AccountFoundriesSection.tsx b/client/src/app/components/nova/address/section/account/AccountFoundriesSection.tsx new file mode 100644 index 000000000..0babb9b14 --- /dev/null +++ b/client/src/app/components/nova/address/section/account/AccountFoundriesSection.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import Pagination from "../../../../Pagination"; +import "./AccountFoundriesSection.scss"; +import TruncatedId from "~/app/components/stardust/TruncatedId"; +import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; + +interface AccountFoundriesSectionProps { + readonly foundries: string[] | null; +} + +const PAGE_SIZE = 10; + +const AccountFoundriesSection: React.FC = ({ foundries }) => { + const { name: network } = useNetworkInfoNova((s) => s.networkInfo); + const isMounted = useIsMounted(); + const [page, setPage] = useState([]); + const [pageNumber, setPageNumber] = useState(1); + + // On page change handler + useEffect(() => { + const from = (pageNumber - 1) * PAGE_SIZE; + const to = from + PAGE_SIZE; + if (isMounted && foundries) { + setPage(foundries.slice(from, to)); + } + }, [foundries, pageNumber]); + + return ( +
+
+
+
Foundry Id
+ {page?.map((foundryId, k) => ( +
+ +
+ ))} +
+
+ + setPageNumber(newPage)} + /> +
+ ); +}; + +export default AccountFoundriesSection; diff --git a/client/src/helpers/nova/hooks/useAccountAddressState.ts b/client/src/helpers/nova/hooks/useAccountAddressState.ts index ae2f555a1..a92f679db 100644 --- a/client/src/helpers/nova/hooks/useAccountAddressState.ts +++ b/client/src/helpers/nova/hooks/useAccountAddressState.ts @@ -8,6 +8,7 @@ import { useNetworkInfoNova } from "../networkInfo"; import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; +import { useAccountControlledFoundries } from "./useAccountControlledFoundries"; export interface IAccountAddressState { addressDetails: IAddressDetails | null; @@ -15,9 +16,11 @@ export interface IAccountAddressState { totalBalance: number | null; availableBalance: number | null; addressBasicOutputs: OutputResponse[] | null; + foundries: string[] | null; isAccountDetailsLoading: boolean; isAssociatedOutputsLoading: boolean; isBasicOutputsLoading: boolean; + isFoundriesLoading: boolean; } const initialState = { @@ -26,9 +29,11 @@ const initialState = { totalBalance: null, availableBalance: null, addressBasicOutputs: null, + foundries: null, isAccountDetailsLoading: true, isAssociatedOutputsLoading: false, isBasicOutputsLoading: false, + isFoundriesLoading: false, }; /** @@ -50,6 +55,7 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres const { accountOutput, isLoading: isAccountDetailsLoading } = useAccountDetails(network, address.accountId); const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, accountOutput); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); + const [foundries, isFoundriesLoading] = useAccountControlledFoundries(network, state.addressDetails); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -69,8 +75,10 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres isAccountDetailsLoading, totalBalance, availableBalance, + foundries, addressBasicOutputs, isBasicOutputsLoading, + isFoundriesLoading, }); }, [accountOutput, totalBalance, availableBalance, addressBasicOutputs, isAccountDetailsLoading, isBasicOutputsLoading]); diff --git a/client/src/helpers/nova/hooks/useAccountControlledFoundries.ts b/client/src/helpers/nova/hooks/useAccountControlledFoundries.ts new file mode 100644 index 000000000..32b7b93c6 --- /dev/null +++ b/client/src/helpers/nova/hooks/useAccountControlledFoundries.ts @@ -0,0 +1,70 @@ +import { OutputType, HexEncodedString, FoundryOutput, Utils } from "@iota/sdk-wasm-nova/web"; +import { useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import { ServiceFactory } from "~factories/serviceFactory"; +import { NOVA } from "~models/config/protocolVersion"; +import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; + +/** + * Fetch Foundries controlled by Account address + * @param network The Network in context + * @param accountAddress The account address + * @returns The account foundries and loading bool. + */ +export function useAccountControlledFoundries(network: string, accountAddress: IAddressDetails | null): [string[] | null, boolean] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [accountFoundries, setAccountFoundries] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (accountAddress) { + const foundries: string[] = []; + // eslint-disable-next-line no-void + void (async () => { + apiClient + .accountFoundries({ + network, + accountAddress: accountAddress.bech32, + }) + .then(async (foundryOutputs) => { + if (foundryOutputs?.foundryOutputsResponse && foundryOutputs?.foundryOutputsResponse?.items.length > 0) { + for (const foundryOutputId of foundryOutputs.foundryOutputsResponse.items) { + const foundryId = await fetchFoundryId(foundryOutputId); + if (foundryId) { + foundries.push(foundryId); + } + } + if (isMounted) { + setAccountFoundries(foundries); + } + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, accountAddress]); + + const fetchFoundryId = async (outputId: HexEncodedString) => { + const foundryId = apiClient.outputDetails({ network, outputId }).then((response) => { + const details = response.output; + if (accountAddress?.hex && !response.error && details?.output?.type === OutputType.Foundry) { + const output = details.output as FoundryOutput; + const serialNumber = output.serialNumber; + const tokenSchemeType = output.tokenScheme.type; + const tokenId = Utils.computeTokenId(accountAddress.hex, serialNumber, tokenSchemeType); + + return tokenId; + } + }); + return foundryId; + }; + + return [accountFoundries, isLoading]; +} diff --git a/client/src/models/api/nova/foundry/IFoundriesRequest.ts b/client/src/models/api/nova/foundry/IFoundriesRequest.ts new file mode 100644 index 000000000..e19631692 --- /dev/null +++ b/client/src/models/api/nova/foundry/IFoundriesRequest.ts @@ -0,0 +1,11 @@ +export interface IFoundriesRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The bech32 account address to get the foundy output ids for. + */ + accountAddress: string; +} diff --git a/client/src/models/api/nova/foundry/IFoundriesResponse.ts b/client/src/models/api/nova/foundry/IFoundriesResponse.ts new file mode 100644 index 000000000..00e860042 --- /dev/null +++ b/client/src/models/api/nova/foundry/IFoundriesResponse.ts @@ -0,0 +1,9 @@ +import { IOutputsResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "../IResponse"; + +export interface IFoundriesResponse extends IResponse { + /** + * The output ids response. + */ + foundryOutputsResponse?: IOutputsResponse; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index ef66bffbe..0ad378576 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -27,6 +27,8 @@ import { ISearchRequest } from "~/models/api/nova/ISearchRequest"; import { ISearchResponse } from "~/models/api/nova/ISearchResponse"; import { IAddressDetailsRequest } from "~/models/api/nova/address/IAddressDetailsRequest"; import { IAddressDetailsResponse } from "~/models/api/nova/address/IAddressDetailsResponse"; +import { IFoundriesResponse } from "~/models/api/nova/foundry/IFoundriesResponse"; +import { IFoundriesRequest } from "~/models/api/nova/foundry/IFoundriesRequest"; /** * Class to handle api communications on nova. @@ -113,6 +115,15 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/foundry/${request.network}/${request.foundryId}`, "get"); } + /** + * Get the foundries controlled by an account address. + * @param request The request to send. + * @returns The response from the request. + */ + public async accountFoundries(request: IFoundriesRequest): Promise { + return this.callApi(`nova/account/foundries/${request.network}/${request.accountAddress}`, "get"); + } + /** * Get the basic outputs details of an address. * @param request The Address Basic outputs request. From 0f0b1c5c77388337d2e897a6ec6a4ff00572c8cb Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Fri, 16 Feb 2024 15:36:58 +0100 Subject: [PATCH 08/11] fix: validation check in foundries endpoint --- api/src/routes/nova/account/foundries/get.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/routes/nova/account/foundries/get.ts b/api/src/routes/nova/account/foundries/get.ts index 77b1775c0..d5f210bbc 100644 --- a/api/src/routes/nova/account/foundries/get.ts +++ b/api/src/routes/nova/account/foundries/get.ts @@ -1,7 +1,6 @@ import { ServiceFactory } from "../../../../factories/serviceFactory"; import { IFoundriesRequest } from "../../../../models/api/nova/foundry/IFoundriesRequest"; import { IFoundriesResponse } from "../../../../models/api/nova/foundry/IFoundriesResponse"; - import { IConfiguration } from "../../../../models/configuration/IConfiguration"; import { NOVA } from "../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../services/networkService"; @@ -18,7 +17,7 @@ export async function get(config: IConfiguration, request: IFoundriesRequest): P const networkService = ServiceFactory.get("network"); const networks = networkService.networkNames(); ValidationHelper.oneOf(request.network, networks, "network"); - ValidationHelper.string(request.accountAddress, "aliasAddress"); + ValidationHelper.string(request.accountAddress, "accountAddress"); const networkConfig = networkService.get(request.network); From 9db3390889bf048085dd5276cf9d01293a3577c2 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Fri, 16 Feb 2024 15:51:41 +0100 Subject: [PATCH 09/11] fix: component imports --- .../app/components/nova/address/AccountAddressView.tsx | 2 +- .../app/components/nova/address/AnchorAddressView.tsx | 2 +- .../app/components/nova/address/Ed25519AddressView.tsx | 2 +- .../nova/address/ImplicitAccountCreationAddressView.tsx | 2 +- .../src/app/components/nova/address/NftAddressView.tsx | 2 +- .../address/section/account/AccountFoundriesSection.tsx | 2 +- .../nova/address/section/native-tokens/Asset.tsx | 9 ++++----- .../nova/address/section/native-tokens/AssetsTable.tsx | 2 +- 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/client/src/app/components/nova/address/AccountAddressView.tsx b/client/src/app/components/nova/address/AccountAddressView.tsx index 248b7c109..51ec141d5 100644 --- a/client/src/app/components/nova/address/AccountAddressView.tsx +++ b/client/src/app/components/nova/address/AccountAddressView.tsx @@ -1,7 +1,7 @@ import { AccountAddress } from "@iota/sdk-wasm-nova/web"; import React from "react"; import { useAccountAddressState } from "~/helpers/nova/hooks/useAccountAddressState"; -import Spinner from "../../Spinner"; +import Spinner from "~/app/components/Spinner"; import Bech32Address from "../../nova/address/Bech32Address"; import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; import AddressBalance from "./AddressBalance"; diff --git a/client/src/app/components/nova/address/AnchorAddressView.tsx b/client/src/app/components/nova/address/AnchorAddressView.tsx index 3c30352f7..c07fd99d2 100644 --- a/client/src/app/components/nova/address/AnchorAddressView.tsx +++ b/client/src/app/components/nova/address/AnchorAddressView.tsx @@ -1,7 +1,7 @@ import { AnchorAddress } from "@iota/sdk-wasm-nova/web"; import React from "react"; import { useAnchorAddressState } from "~/helpers/nova/hooks/useAnchorAddressState"; -import Spinner from "../../Spinner"; +import Spinner from "~/app/components/Spinner"; import AddressBalance from "./AddressBalance"; import Bech32Address from "./Bech32Address"; import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; diff --git a/client/src/app/components/nova/address/Ed25519AddressView.tsx b/client/src/app/components/nova/address/Ed25519AddressView.tsx index 6c89a5717..375adcfa8 100644 --- a/client/src/app/components/nova/address/Ed25519AddressView.tsx +++ b/client/src/app/components/nova/address/Ed25519AddressView.tsx @@ -4,7 +4,7 @@ import { useEd25519AddressState } from "~/helpers/nova/hooks/useEd25519AddressSt import AddressBalance from "./AddressBalance"; import Bech32Address from "./Bech32Address"; import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; -import Spinner from "../../Spinner"; +import Spinner from "~/app/components/Spinner"; interface Ed25519AddressViewProps { ed25519Address: Ed25519Address; diff --git a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx index 05e0256ab..60162fdc5 100644 --- a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx +++ b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx @@ -3,7 +3,7 @@ import React from "react"; import { useImplicitAccountCreationAddressState } from "~/helpers/nova/hooks/useImplicitAccountCreationAddressState"; import AddressBalance from "./AddressBalance"; import Bech32Address from "./Bech32Address"; -import Spinner from "../../Spinner"; +import Spinner from "~/app/components/Spinner"; import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; interface ImplicitAccountCreationAddressViewProps { diff --git a/client/src/app/components/nova/address/NftAddressView.tsx b/client/src/app/components/nova/address/NftAddressView.tsx index 51431739e..3646e9ddb 100644 --- a/client/src/app/components/nova/address/NftAddressView.tsx +++ b/client/src/app/components/nova/address/NftAddressView.tsx @@ -1,7 +1,7 @@ import { NftAddress } from "@iota/sdk-wasm-nova/web"; import React from "react"; import { useNftAddressState } from "~/helpers/nova/hooks/useNftAddressState"; -import Spinner from "../../Spinner"; +import Spinner from "~/app/components/Spinner"; import AddressBalance from "./AddressBalance"; import Bech32Address from "./Bech32Address"; import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections"; diff --git a/client/src/app/components/nova/address/section/account/AccountFoundriesSection.tsx b/client/src/app/components/nova/address/section/account/AccountFoundriesSection.tsx index 0babb9b14..d0fd20aaa 100644 --- a/client/src/app/components/nova/address/section/account/AccountFoundriesSection.tsx +++ b/client/src/app/components/nova/address/section/account/AccountFoundriesSection.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { useIsMounted } from "~helpers/hooks/useIsMounted"; -import Pagination from "../../../../Pagination"; +import Pagination from "~/app/components/Pagination"; import "./AccountFoundriesSection.scss"; import TruncatedId from "~/app/components/stardust/TruncatedId"; import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; diff --git a/client/src/app/components/nova/address/section/native-tokens/Asset.tsx b/client/src/app/components/nova/address/section/native-tokens/Asset.tsx index c22f40bcf..426614915 100644 --- a/client/src/app/components/nova/address/section/native-tokens/Asset.tsx +++ b/client/src/app/components/nova/address/section/native-tokens/Asset.tsx @@ -1,14 +1,13 @@ /* eslint-disable jsdoc/require-param */ /* eslint-disable jsdoc/require-returns */ -import { FoundryOutput, MetadataFeature, FeatureType, Irc30Metadata } from "@iota/sdk-wasm-nova/web"; +import { FoundryOutput, MetadataFeature, FeatureType, Irc30Metadata, hexToUtf8 } from "@iota/sdk-wasm-nova/web"; import { Validator as JsonSchemaValidator } from "jsonschema"; import React, { ReactElement, useEffect, useState } from "react"; import { AssetProps } from "./AssetProps"; import tokenSchemeIRC30 from "~assets/schemas/token-schema-IRC30.json"; -import { useFoundryDetails } from "~helpers/stardust/hooks/useFoundryDetails"; +import { useFoundryDetails } from "~helpers/nova/hooks/useFoundryDetails"; import { useTokenRegistryNativeTokenCheck } from "~helpers/stardust/hooks/useTokenRegistryNativeTokenCheck"; -import { Converter } from "~helpers/stardust/convertUtils"; -import Spinner from "../../../../Spinner"; +import Spinner from "~/app/components/Spinner"; import TruncatedId from "~/app/components/stardust/TruncatedId"; import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; @@ -37,7 +36,7 @@ const Asset: React.FC = ({ tableFormat, token }) => { const validator = new JsonSchemaValidator(); try { - const tokenInfo = JSON.parse(Converter.hexToUtf8(metadata.data)) as Irc30Metadata; + const tokenInfo = JSON.parse(hexToUtf8(metadata.data)) as Irc30Metadata; const result = validator.validate(tokenInfo, tokenSchemeIRC30); if (result.valid) { diff --git a/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx b/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx index 99e13dc7a..bce41a33c 100644 --- a/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx +++ b/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx @@ -1,7 +1,7 @@ import { OutputType, OutputResponse, CommonOutput, INativeToken, FeatureType, NativeTokenFeature } from "@iota/sdk-wasm-nova/web"; import React, { useEffect, useState } from "react"; import Asset from "./Asset"; -import Pagination from "../../../../Pagination"; +import Pagination from "~/app/components/Pagination"; import { plainToInstance } from "class-transformer"; import "./AssetsTable.scss"; From f2e9aea3784e49e07bd4f9a198065a0540abbfda Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Fri, 16 Feb 2024 16:20:24 +0100 Subject: [PATCH 10/11] fix: imports --- .../nova/address/section/native-tokens/AssetProps.tsx | 4 ++-- .../nova/address/section/native-tokens/AssetsTable.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/app/components/nova/address/section/native-tokens/AssetProps.tsx b/client/src/app/components/nova/address/section/native-tokens/AssetProps.tsx index 5d1dc8511..34bf43384 100644 --- a/client/src/app/components/nova/address/section/native-tokens/AssetProps.tsx +++ b/client/src/app/components/nova/address/section/native-tokens/AssetProps.tsx @@ -1,10 +1,10 @@ -import { IToken } from "~models/api/stardust/foundry/IToken"; +import { NativeToken } from "@iota/sdk-wasm-nova/web"; export interface AssetProps { /** * Token */ - token: IToken; + token: NativeToken; /** * True if the asset is rendered like a table diff --git a/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx b/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx index bce41a33c..011e35dcf 100644 --- a/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx +++ b/client/src/app/components/nova/address/section/native-tokens/AssetsTable.tsx @@ -1,4 +1,4 @@ -import { OutputType, OutputResponse, CommonOutput, INativeToken, FeatureType, NativeTokenFeature } from "@iota/sdk-wasm-nova/web"; +import { OutputType, OutputResponse, CommonOutput, NativeToken, FeatureType, NativeTokenFeature } from "@iota/sdk-wasm-nova/web"; import React, { useEffect, useState } from "react"; import Asset from "./Asset"; import Pagination from "~/app/components/Pagination"; @@ -13,8 +13,8 @@ interface AssetsTableProps { const TOKEN_PAGE_SIZE: number = 10; const AssetsTable: React.FC = ({ outputs, setTokensCount }) => { - const [tokens, setTokens] = useState(); - const [currentPage, setCurrentPage] = useState([]); + const [tokens, setTokens] = useState(); + const [currentPage, setCurrentPage] = useState([]); const [pageNumber, setPageNumber] = useState(1); useEffect(() => { @@ -23,7 +23,7 @@ const AssetsTable: React.FC = ({ outputs, setTokensCount }) => } if (outputs) { - const theTokens: INativeToken[] = []; + const theTokens: NativeToken[] = []; for (const outputResponse of outputs) { const output = outputResponse.output as CommonOutput; if (output.type === OutputType.Basic || output.type === OutputType.Foundry) { @@ -102,7 +102,7 @@ const AssetsTable: React.FC = ({ outputs, setTokensCount }) => }; AssetsTable.defaultProps = { - setTokenCount: undefined, + setTokensCount: undefined, }; export default AssetsTable; From babaf502120e2f20bd065be93530538adcce365b Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Sun, 18 Feb 2024 13:38:41 +0100 Subject: [PATCH 11/11] fix: select first tab on address page --- .../nova/address/section/AddressPageTabbedSections.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx index a57cf358e..29410a48d 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -14,8 +14,8 @@ import { AddressType } from "@iota/sdk-wasm-nova/web"; import AccountFoundriesSection from "./account/AccountFoundriesSection"; enum DEFAULT_TABS { - NativeTokens = "Native Tokens", AssocOutputs = "Outputs", + NativeTokens = "Native Tokens", } enum ACCOUNT_TABS { @@ -66,13 +66,13 @@ export const AddressPageTabbedSections: React.FC, , + , ]; const accountAddressSections =