From d2c6fb0642a0549e0a047d8f4b9c50083db0339e Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Tue, 19 Dec 2023 12:48:11 +0100 Subject: [PATCH 01/15] feat: Add infra to support OutputPage (nova) --- .../models/api/nova/IOutputDetailsResponse.ts | 9 +++ api/src/models/api/nova/IResponse.ts | 11 +++ api/src/routes.ts | 4 +- api/src/routes/nova/output/get.ts | 32 +++++++++ api/src/services/nova/novaApi.ts | 67 +++++++++++++++++++ client/package.json | 5 +- client/src/app/routes.tsx | 7 +- client/src/app/routes/nova/OutputPage.tsx | 45 +++++++++++++ .../helpers/nova/hooks/useOutputDetails.ts | 56 ++++++++++++++++ .../models/api/nova/IOutputDetailsResponse.ts | 9 +++ client/src/models/api/nova/IResponse.ts | 11 +++ client/src/services/nova/novaApiClient.ts | 13 ++++ 12 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 api/src/models/api/nova/IOutputDetailsResponse.ts create mode 100644 api/src/models/api/nova/IResponse.ts create mode 100644 api/src/routes/nova/output/get.ts create mode 100644 api/src/services/nova/novaApi.ts create mode 100644 client/src/app/routes/nova/OutputPage.tsx create mode 100644 client/src/helpers/nova/hooks/useOutputDetails.ts create mode 100644 client/src/models/api/nova/IOutputDetailsResponse.ts create mode 100644 client/src/models/api/nova/IResponse.ts diff --git a/api/src/models/api/nova/IOutputDetailsResponse.ts b/api/src/models/api/nova/IOutputDetailsResponse.ts new file mode 100644 index 000000000..abe29fde2 --- /dev/null +++ b/api/src/models/api/nova/IOutputDetailsResponse.ts @@ -0,0 +1,9 @@ +import { OutputResponse } from "@iota/sdk-nova"; +import { IResponse } from "./IResponse"; + +export interface IOutputDetailsResponse extends IResponse { + /** + * The output data. + */ + output?: OutputResponse; +} diff --git a/api/src/models/api/nova/IResponse.ts b/api/src/models/api/nova/IResponse.ts new file mode 100644 index 000000000..285ca58b9 --- /dev/null +++ b/api/src/models/api/nova/IResponse.ts @@ -0,0 +1,11 @@ +export interface IResponse { + /** + * An error for the response. + */ + error?: string; + + /** + * A message for the response. + */ + message?: string; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index d90417965..b8ff7ced3 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -142,5 +142,7 @@ export const routes: IRoute[] = [ { path: "/stardust/token/distribution/:network", method: "get", folder: "stardust/address/distribution", func: "get" - } + }, + // Nova + { path: "/nova/output/:network/:outputId", method: "get", folder: "nova/output", func: "get" } ]; diff --git a/api/src/routes/nova/output/get.ts b/api/src/routes/nova/output/get.ts new file mode 100644 index 000000000..8dd204880 --- /dev/null +++ b/api/src/routes/nova/output/get.ts @@ -0,0 +1,32 @@ +import { ServiceFactory } from "../../../factories/serviceFactory"; +import { IOutputDetailsRequest } from "../../../models/api/chrysalis/IOutputDetailsRequest"; +import { IOutputDetailsResponse } from "../../../models/api/nova/IOutputDetailsResponse"; +import { IConfiguration } from "../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../models/db/protocolVersion"; +import { NetworkService } from "../../../services/networkService"; +import { NovaApi } from "../../../services/nova/novaApi"; +import { ValidationHelper } from "../../../utils/validationHelper"; + +/** + * Find the object from the network. + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get( + config: IConfiguration, + request: IOutputDetailsRequest +): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.outputId, "outputId"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + return NovaApi.outputDetails(networkConfig, request.outputId); +} diff --git a/api/src/services/nova/novaApi.ts b/api/src/services/nova/novaApi.ts new file mode 100644 index 000000000..87f424381 --- /dev/null +++ b/api/src/services/nova/novaApi.ts @@ -0,0 +1,67 @@ +import { + __ClientMethods__, OutputResponse, Client +} from "@iota/sdk-nova"; +import { ServiceFactory } from "../../factories/serviceFactory"; +import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; +import { INetwork } from "../../models/db/INetwork"; + +type NameType = T extends { name: infer U } ? U : never; +type ExtractedMethodNames = NameType<__ClientMethods__>; + +/** + * Class to interact with the nova API. + */ +export class NovaApi { + /** + * Get the output details. + * @param network The network to find the items on. + * @param outputId The output id to get the details. + * @returns The item details. + */ + public static async outputDetails(network: INetwork, outputId: string): Promise { + const outputResponse = await this.tryFetchNodeThenPermanode( + outputId, + "getOutput", + network + ); + + return outputResponse ? + { output: outputResponse } : + { message: "Output not found" }; + } + + /** + * Generic helper function to try fetching from node client. + * On failure (or not present), we try to fetch from permanode (if configured). + * @param args The argument(s) to pass to the fetch calls. + * @param methodName The function to call on the client. + * @param network The network config in context. + * @returns The results or null if call(s) failed. + */ + public static async tryFetchNodeThenPermanode( + args: A, + methodName: ExtractedMethodNames, + network: INetwork + ): Promise | null { + const { permaNodeEndpoint, disableApiFallback } = network; + const isFallbackEnabled = !disableApiFallback; + const client = ServiceFactory.get(`client-${network.network}`); + + try { + // try fetch from node + const result: Promise = client[methodName](args); + return await result; + } catch { } + + if (permaNodeEndpoint && isFallbackEnabled) { + const permanodeClient = ServiceFactory.get(`permanode-client-${network.network}`); + try { + // try fetch from permanode (chronicle) + const result: Promise = permanodeClient[methodName](args); + return await result; + } catch { } + } + + return null; + } +} diff --git a/client/package.json b/client/package.json index 9edd434bc..dbd873e5c 100644 --- a/client/package.json +++ b/client/package.json @@ -114,5 +114,8 @@ "not dead", "not ie <= 11", "not op_mini all" - ] + ], + "prettier": { + "tabWidth": 4 + } } diff --git a/client/src/app/routes.tsx b/client/src/app/routes.tsx index d5ae92552..61ef11b54 100644 --- a/client/src/app/routes.tsx +++ b/client/src/app/routes.tsx @@ -32,6 +32,7 @@ import { Landing as StardustLanding } from "./routes/stardust/landing/Landing"; import NftRedirectRoute from "./routes/stardust/NftRedirectRoute"; import StardustOutputList from "./routes/stardust/OutputList"; import StardustOutputPage from "./routes/stardust/OutputPage"; +import NovaOutputPage from "./routes/nova/OutputPage"; import StardustSearch from "./routes/stardust/Search"; import StardustStatisticsPage from "./routes/stardust/statistics/StatisticsPage"; import StardustTransactionPage from "./routes/stardust/TransactionPage"; @@ -213,7 +214,11 @@ const buildAppRoutes = ( + />, + , ]; return ( diff --git a/client/src/app/routes/nova/OutputPage.tsx b/client/src/app/routes/nova/OutputPage.tsx new file mode 100644 index 000000000..b613960a8 --- /dev/null +++ b/client/src/app/routes/nova/OutputPage.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { RouteComponentProps } from "react-router-dom"; +import NotFound from "~/app/components/NotFound"; +import { useOutputDetails } from "~/helpers/nova/hooks/useOutputDetails"; + +interface OutputPageProps { + /** + * The network to lookup. + */ + network: string; + + /** + * The output id to lookup. + */ + outputId: string; +} + +const OutputPage: React.FC> = ({ + match: { + params: { network, outputId }, + }, +}) => { + const [output, , , outputError] = useOutputDetails(network, outputId); + + if (outputError) { + return ( +
+
+
+
+
+

Output

+
+
+ +
+
+
+ ); + } + + return
Ze output {JSON.stringify(output)}
; +}; + +export default OutputPage; diff --git a/client/src/helpers/nova/hooks/useOutputDetails.ts b/client/src/helpers/nova/hooks/useOutputDetails.ts new file mode 100644 index 000000000..6774bc2c1 --- /dev/null +++ b/client/src/helpers/nova/hooks/useOutputDetails.ts @@ -0,0 +1,56 @@ +import { IOutputMetadataResponse, Output } from "@iota/sdk-wasm-nova/web"; +import { useEffect, useState } from "react"; +import { ServiceFactory } from "~/factories/serviceFactory"; +import { useIsMounted } from "~/helpers/hooks/useIsMounted"; +import { HexHelper } from "~/helpers/stardust/hexHelper"; +import { NOVA } from "~/models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; + +/** + * Fetch output details + * @param network The Network in context + * @param outputId The output id + * @returns The output, metadata, loading bool and error message. + */ +export function useOutputDetails(network: string, outputId: string | null): + [ + Output | null, + IOutputMetadataResponse | null, + boolean, + string? + ] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [output, setOutput] = useState(null); + const [metadata, setMetadata] = useState(null); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + setOutput(null); + setMetadata(null); + if (outputId) { + // eslint-disable-next-line no-void + void (async () => { + apiClient.outputDetails({ + network, + outputId: HexHelper.addPrefix(outputId) + }).then(response => { + if (isMounted) { + const details = response.output; + setOutput(details?.output ?? null); + setMetadata(details?.metadata ?? null); + setError(response.error); + } + }).finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, outputId]); + + return [output, metadata, isLoading, error]; +} diff --git a/client/src/models/api/nova/IOutputDetailsResponse.ts b/client/src/models/api/nova/IOutputDetailsResponse.ts new file mode 100644 index 000000000..5b749c3e1 --- /dev/null +++ b/client/src/models/api/nova/IOutputDetailsResponse.ts @@ -0,0 +1,9 @@ +import { OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "./IResponse"; + +export interface IOutputDetailsResponse extends IResponse { + /** + * The output data. + */ + output?: OutputResponse; +} diff --git a/client/src/models/api/nova/IResponse.ts b/client/src/models/api/nova/IResponse.ts new file mode 100644 index 000000000..285ca58b9 --- /dev/null +++ b/client/src/models/api/nova/IResponse.ts @@ -0,0 +1,11 @@ +export interface IResponse { + /** + * An error for the response. + */ + error?: string; + + /** + * A message for the response. + */ + message?: string; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index b318c0b19..ee72a84a4 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -1,5 +1,7 @@ import { INetworkBoundGetRequest } from "~/models/api/INetworkBoundGetRequest"; +import { IOutputDetailsRequest } from "~/models/api/IOutputDetailsRequest"; import { INodeInfoResponse } from "~/models/api/nova/INodeInfoResponse"; +import { IOutputDetailsResponse } from "~/models/api/nova/IOutputDetailsResponse"; import { ApiClient } from "../apiClient"; /** @@ -17,4 +19,15 @@ export class NovaApiClient extends ApiClient { "get" ); } + + /** + * Get the output details. + * @param request The request to send. + * @returns The response from the request. + */ + public async outputDetails(request: IOutputDetailsRequest): Promise { + return this.callApi( + `nova/output/${request.network}/${request.outputId}`, "get" + ); + } } From 8d1d4e27537a159b80e767f35e56e67c31517a39 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Wed, 20 Dec 2023 17:56:15 +0100 Subject: [PATCH 02/15] feat: add block page for nova --- api/src/models/api/nova/IBlockResponse.ts | 9 ++++ api/src/routes.ts | 3 +- api/src/routes/nova/block/get.ts | 32 ++++++++++++ api/src/services/nova/novaApi.ts | 35 ++++++++++++- client/src/app/routes.tsx | 5 ++ client/src/app/routes/nova/Block.tsx | 52 +++++++++++++++++++ client/src/helpers/nova/hooks/useBlock.ts | 52 +++++++++++++++++++ .../models/api/nova/block/IBlockRequest.ts | 12 +++++ .../models/api/nova/block/IBlockResponse.ts | 10 ++++ client/src/services/nova/novaApiClient.ts | 13 +++++ 10 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 api/src/models/api/nova/IBlockResponse.ts create mode 100644 api/src/routes/nova/block/get.ts create mode 100644 client/src/app/routes/nova/Block.tsx create mode 100644 client/src/helpers/nova/hooks/useBlock.ts create mode 100644 client/src/models/api/nova/block/IBlockRequest.ts create mode 100644 client/src/models/api/nova/block/IBlockResponse.ts diff --git a/api/src/models/api/nova/IBlockResponse.ts b/api/src/models/api/nova/IBlockResponse.ts new file mode 100644 index 000000000..6f42e816f --- /dev/null +++ b/api/src/models/api/nova/IBlockResponse.ts @@ -0,0 +1,9 @@ +import { Block } from "@iota/sdk-nova"; +import { IResponse } from "./IResponse"; + +export interface IBlockResponse extends IResponse { + /** + * The deserialized block. + */ + block?: Block; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index b8ff7ced3..5b28d88c5 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -144,5 +144,6 @@ export const routes: IRoute[] = [ folder: "stardust/address/distribution", func: "get" }, // Nova - { path: "/nova/output/:network/:outputId", method: "get", folder: "nova/output", func: "get" } + { path: "/nova/output/:network/:outputId", method: "get", folder: "nova/output", func: "get" }, + { path: "/nova/block/:network/:blockId", method: "get", folder: "nova/block", func: "get" } ]; diff --git a/api/src/routes/nova/block/get.ts b/api/src/routes/nova/block/get.ts new file mode 100644 index 000000000..61e71b230 --- /dev/null +++ b/api/src/routes/nova/block/get.ts @@ -0,0 +1,32 @@ +import { ServiceFactory } from "../../../factories/serviceFactory"; +import { IBlockResponse } from "../../../models/api/nova/IBlockResponse"; +import { IBlockRequest } from "../../../models/api/stardust/IBlockRequest"; +import { IConfiguration } from "../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../models/db/protocolVersion"; +import { NetworkService } from "../../../services/networkService"; +import { NovaApi } from "../../../services/nova/novaApi"; +import { ValidationHelper } from "../../../utils/validationHelper"; + +/** + * Fetch the block from the network. + * @param _ The configuration. + * @param request The request. + * @returns The response. + */ +export async function get( + _: IConfiguration, + request: IBlockRequest +): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.blockId, "blockId"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + return NovaApi.block(networkConfig, request.blockId); +} diff --git a/api/src/services/nova/novaApi.ts b/api/src/services/nova/novaApi.ts index 87f424381..c1184c107 100644 --- a/api/src/services/nova/novaApi.ts +++ b/api/src/services/nova/novaApi.ts @@ -1,9 +1,12 @@ import { - __ClientMethods__, OutputResponse, Client + __ClientMethods__, OutputResponse, Client, Block } from "@iota/sdk-nova"; import { ServiceFactory } from "../../factories/serviceFactory"; +import logger from "../../logger"; +import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; import { INetwork } from "../../models/db/INetwork"; +import { HexHelper } from "../../utils/hexHelper"; type NameType = T extends { name: infer U } ? U : never; type ExtractedMethodNames = NameType<__ClientMethods__>; @@ -12,6 +15,36 @@ type ExtractedMethodNames = NameType<__ClientMethods__>; * Class to interact with the nova API. */ export class NovaApi { + /** + * Get a block. + * @param network The network to find the items on. + * @param blockId The block id to get the details. + * @returns The block response. + */ + public static async block(network: INetwork, blockId: string): Promise { + blockId = HexHelper.addPrefix(blockId); + const block = await this.tryFetchNodeThenPermanode( + blockId, + "getBlock", + network + ); + + if (!block) { + return { error: `Couldn't find block with id ${blockId}` }; + } + + try { + if (block && Object.keys(block).length > 0) { + return { + block + }; + } + } catch (e) { + logger.error(`Failed fetching block with block id ${blockId}. Cause: ${e}`); + return { error: "Block fetch failed." }; + } + } + /** * Get the output details. * @param network The network to find the items on. diff --git a/client/src/app/routes.tsx b/client/src/app/routes.tsx index 61ef11b54..2cd2beb73 100644 --- a/client/src/app/routes.tsx +++ b/client/src/app/routes.tsx @@ -32,6 +32,7 @@ import { Landing as StardustLanding } from "./routes/stardust/landing/Landing"; import NftRedirectRoute from "./routes/stardust/NftRedirectRoute"; import StardustOutputList from "./routes/stardust/OutputList"; import StardustOutputPage from "./routes/stardust/OutputPage"; +import NovaBlockPage from "./routes/nova/Block"; import NovaOutputPage from "./routes/nova/OutputPage"; import StardustSearch from "./routes/stardust/Search"; import StardustStatisticsPage from "./routes/stardust/statistics/StatisticsPage"; @@ -215,6 +216,10 @@ const buildAppRoutes = ( key={keys.next().value} component={NovaVisualizer} />, + , > = ( + { history, match: { params: { network, blockId } } } +) => { + + const [block, ,blockError] = useBlock(network, blockId); + + if (blockError) { + return ( +
+
+
+ +
+
+
+ ); + } + + return ( +
+
+
+
{JSON.stringify(block)}
+
+
+
+ ); +}; + +export default Block; + diff --git a/client/src/helpers/nova/hooks/useBlock.ts b/client/src/helpers/nova/hooks/useBlock.ts new file mode 100644 index 000000000..80a72c542 --- /dev/null +++ b/client/src/helpers/nova/hooks/useBlock.ts @@ -0,0 +1,52 @@ +import { Block } 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"; +import { HexHelper } from "~/helpers/stardust/hexHelper"; + +/** + * Fetch the block + * @param network The Network in context + * @param blockId The block id + * @returns The block, loading bool and an error message. + */ +export function useBlock(network: string, blockId: string | null): + [ + Block | null, + boolean, + string? + ] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [block, setBlock] = useState(null); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + console.log("useBlock nova") + setIsLoading(true); + setBlock(null); + if (blockId) { + // eslint-disable-next-line no-void + void (async () => { + apiClient.block({ + network, + blockId: HexHelper.addPrefix(blockId) + }).then(response => { + if (isMounted) { + setBlock(response.block ?? null); + setError(response.error); + } + }).finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, blockId]); + + return [block, isLoading, error]; +} diff --git a/client/src/models/api/nova/block/IBlockRequest.ts b/client/src/models/api/nova/block/IBlockRequest.ts new file mode 100644 index 000000000..e22b99aab --- /dev/null +++ b/client/src/models/api/nova/block/IBlockRequest.ts @@ -0,0 +1,12 @@ +export interface IBlockRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The block id to fetch. + */ + blockId: string; +} + diff --git a/client/src/models/api/nova/block/IBlockResponse.ts b/client/src/models/api/nova/block/IBlockResponse.ts new file mode 100644 index 000000000..a688d366d --- /dev/null +++ b/client/src/models/api/nova/block/IBlockResponse.ts @@ -0,0 +1,10 @@ +import { Block } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "../IResponse"; + +export interface IBlockResponse extends IResponse { + /** + * The deserialized block. + */ + block: Block; +} + diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index ee72a84a4..d4d75a0da 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -1,4 +1,6 @@ import { INetworkBoundGetRequest } from "~/models/api/INetworkBoundGetRequest"; +import { IBlockRequest } from "~/models/api/nova/block/IBlockRequest"; +import { IBlockResponse } from "~/models/api/nova/block/IBlockResponse"; import { IOutputDetailsRequest } from "~/models/api/IOutputDetailsRequest"; import { INodeInfoResponse } from "~/models/api/nova/INodeInfoResponse"; import { IOutputDetailsResponse } from "~/models/api/nova/IOutputDetailsResponse"; @@ -20,6 +22,17 @@ export class NovaApiClient extends ApiClient { ); } + /** + * Get a block. + * @param request The request to send. + * @returns The response from the request. + */ + public async block(request: IBlockRequest): Promise { + return this.callApi( + `nova/block/${request.network}/${request.blockId}`, "get" + ); + } + /** * Get the output details. * @param request The request to send. From c19c98d46b66319de326cdf0c107a6f49f04a621 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Mon, 8 Jan 2024 16:07:08 +0100 Subject: [PATCH 03/15] fix: Add initial Block page structure --- client/src/app/routes/nova/Block.tsx | 141 ++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index d9c791fc2..84a375d87 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -1,8 +1,15 @@ import React from "react"; import { RouteComponentProps } from "react-router-dom"; +import mainHeaderMessage from "~assets/modals/stardust/block/main-header.json"; import { useBlock } from "~helpers/nova/hooks/useBlock"; import NotFound from "../../components/NotFound"; +import { BlockBodyType } from "@iota/sdk-wasm-nova/web"; +import Modal from "~/app/components/Modal"; +import Spinner from "~/app/components/Spinner"; +import TruncatedId from "~/app/components/stardust/TruncatedId"; +import { DateHelper } from "~/helpers/dateHelper"; +import MilestoneSignaturesSection from "~/app/components/stardust/block/payload/milestone/MilestoneSignaturesSection"; export interface BlockProps { /** @@ -20,13 +27,134 @@ const Block: React.FC> = ( { history, match: { params: { network, blockId } } } ) => { - const [block, ,blockError] = useBlock(network, blockId); + const [block, isLoading, blockError] = useBlock(network, blockId); + + let pageTitle = "Block"; + switch (block?.body?.type) { + case BlockBodyType.Basic: { + pageTitle = `Basic ${pageTitle}`; + break; + } + case BlockBodyType.Validation: { + pageTitle = `Validation ${pageTitle}`; + break; + } + default: { + break; + } + } + const blockContent = block ? ( + +
+
+

General

+
+
+
+
+ Block ID +
+
+ +
+
+
+
+ Issuing Time +
+
+ {/* Convert nanoseconds to milliseconds */} + {DateHelper.formatShort(Number(block.header.issuingTime) / 1000000)} +
+
+
+
+ Slot commitment +
+
+ +
+
+
+
+ Issuer +
+
+ +
+
+ +
+ {block.body?.strongParents && ( +
+
+ Strong Parents +
+ {block.body?.strongParents.map((parent, idx) => ( +
+ +
+ ))} +
+ )} + {block.body?.weakParents && ( +
+
+ Weak Parents +
+ {block.body?.weakParents.map((child, idx) => ( +
+ +
+ ))} +
+ )} +
+ + {block.body.type === BlockBodyType.Basic && ( +
+ {JSON.stringify(block.body)} +
+
+ Max burned mana +
+
+ {block.body.maxBurnedMana} +
+
+
+ )} + +
+ ) : null; if (blockError) { return (
+
+
+

+ {pageTitle} +

+ +
+
> = (
-
{JSON.stringify(block)}
+
+
+
+

{pageTitle}

+ + {isLoading && } +
+
+
+
{blockContent}
From 11ed337e711480a0f06029e32e3756e5982ef4a2 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Mon, 8 Jan 2024 16:21:51 +0100 Subject: [PATCH 04/15] fix: Add initial Block page structure --- client/src/app/routes/nova/Block.tsx | 31 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index 84a375d87..2a97e4674 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -4,12 +4,13 @@ import { RouteComponentProps } from "react-router-dom"; import mainHeaderMessage from "~assets/modals/stardust/block/main-header.json"; import { useBlock } from "~helpers/nova/hooks/useBlock"; import NotFound from "../../components/NotFound"; -import { BlockBodyType } from "@iota/sdk-wasm-nova/web"; +import { BasicBlockBody, BlockBodyType, ValidationBlockBody } from "@iota/sdk-wasm-nova/web"; import Modal from "~/app/components/Modal"; import Spinner from "~/app/components/Spinner"; import TruncatedId from "~/app/components/stardust/TruncatedId"; import { DateHelper } from "~/helpers/dateHelper"; import MilestoneSignaturesSection from "~/app/components/stardust/block/payload/milestone/MilestoneSignaturesSection"; +import { Ed25519Signature } from "@iota/sdk-wasm/web"; export interface BlockProps { /** @@ -29,20 +30,25 @@ const Block: React.FC> = ( const [block, isLoading, blockError] = useBlock(network, blockId); + let blockBody: BasicBlockBody | ValidationBlockBody | undefined let pageTitle = "Block"; switch (block?.body?.type) { case BlockBodyType.Basic: { pageTitle = `Basic ${pageTitle}`; + blockBody = blockBody as BasicBlockBody break; } case BlockBodyType.Validation: { pageTitle = `Validation ${pageTitle}`; + blockBody = block?.body as ValidationBlockBody break; } default: { break; } } + + const blockContent = block ? (
@@ -85,12 +91,12 @@ const Block: React.FC> = (
- {block.body?.strongParents && ( + {blockBody?.strongParents && (
Strong Parents
- {block.body?.strongParents.map((parent, idx) => ( + {blockBody.strongParents.map((parent, idx) => (
> = ( ))}
)} - {block.body?.weakParents && ( + {blockBody?.weakParents && (
Weak Parents
- {block.body?.weakParents.map((child, idx) => ( + {blockBody.weakParents.map((child, idx) => (
> = ( )}
- {block.body.type === BlockBodyType.Basic && ( + {block.body?.type === BlockBodyType.Basic && (
- {JSON.stringify(block.body)} -
-
- Max burned mana -
-
- {block.body.maxBurnedMana} -
-
+ {/* todo: add all block payload */} + {JSON.stringify(blockBody)}
)} - + ) : null; From ade3e450bcd786a5918e3c7d6c0dd8bea6569e0b Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Thu, 11 Jan 2024 14:48:33 +0100 Subject: [PATCH 05/15] feat: Add View for tagged data payload and signed transaction payload --- client/src/app/components/nova/Unlocks.tsx | 113 ++++++++++++++++++ .../payload/SignedTransactionPayload.tsx | 70 +++++++++++ .../nova/block/payload/TaggedDataPayload.tsx | 49 ++++++++ client/src/app/routes/nova/Block.tsx | 3 +- client/src/helpers/nova/nameHelper.ts | 37 ++++++ 5 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 client/src/app/components/nova/Unlocks.tsx create mode 100644 client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx create mode 100644 client/src/app/components/nova/block/payload/TaggedDataPayload.tsx create mode 100644 client/src/helpers/nova/nameHelper.ts diff --git a/client/src/app/components/nova/Unlocks.tsx b/client/src/app/components/nova/Unlocks.tsx new file mode 100644 index 000000000..03899dd6d --- /dev/null +++ b/client/src/app/components/nova/Unlocks.tsx @@ -0,0 +1,113 @@ +import { AccountUnlock, AnchorUnlock, EmptyUnlock, MultiUnlock, NftUnlock, ReferenceUnlock, SignatureUnlock, Unlock, UnlockType } from "@iota/sdk-wasm-nova/web"; +import classNames from "classnames"; +import React, { useState } from "react"; +import DropdownIcon from "~assets/dropdown-arrow.svg?react"; +import TruncatedId from "../stardust/TruncatedId"; +import { NameHelper } from "~helpers/nova/nameHelper"; + +interface IUnlocksProps { + readonly unlocks: Unlock[]; +} + +interface UnlockTypeMap { + [UnlockType.Signature]: SignatureUnlock; + [UnlockType.Reference]: ReferenceUnlock; + [UnlockType.Account]: AccountUnlock; + [UnlockType.Anchor]: AnchorUnlock; + [UnlockType.Nft]: NftUnlock; + [UnlockType.Multi]: MultiUnlock; + [UnlockType.Empty]: EmptyUnlock; +} + +const Unlocks: React.FC = ({ unlocks }) => { + const [isExpanded, setIsExpanded] = useState(false); + + const displayUnlocksTypeAndIndex = (type: number, index: number) => ( +
+ {type !== UnlockType.Empty && ( +
+ Index: + {index} +
+ )} +
+ Type: + {NameHelper.getUnlockTypeName(type)} +
+
+ ); + + return ( +
+
setIsExpanded(!isExpanded)} + className="card--value card-header--wrapper" + > +
+ +
+
+ +
+
+ { + isExpanded && ( +
+ {unlocks.map((unlock, idx) => { + if (unlock.type === UnlockType.Signature) { + const signatureUnlock = unlock as SignatureUnlock; + + return ( +
+ {displayUnlocksTypeAndIndex(unlock.type, idx)} +
+ Public Key: +
+ +
+
+
+ Signature: +
+ +
+
+
+ ); + } else if (unlock.type === UnlockType.Multi) { + const multiUnlock = unlock as MultiUnlock; + + return ; + } + else if (unlock.type === UnlockType.Empty) { + return ( +
+ {displayUnlocksTypeAndIndex(unlock.type, idx)} +
+ ); + } + else { + const referencedUnlock = unlock as UnlockTypeMap[typeof unlock.type]; + + return ( +
+ {displayUnlocksTypeAndIndex(unlock.type, idx)} +
+ References unlock at index: + {referencedUnlock.reference} +
+
+ ); + } + })} +
+ ) + } +
+ ); +}; + +export default Unlocks; + diff --git a/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx b/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx new file mode 100644 index 000000000..284eebf8e --- /dev/null +++ b/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx @@ -0,0 +1,70 @@ +import { SignedTransactionPayload as ISignedTransactionPayload, Utils } from "@iota/sdk-wasm-nova/web"; +import React from "react"; +import Modal from "~/app/components/Modal"; +import Unlocks from "~/app/components/nova/Unlocks"; +import OutputView from "~/app/components/nova/OutputView"; +import transactionPayloadMessage from "~assets/modals/stardust/block/transaction-payload.json"; + +interface SignedTransactionPayloadProps { + readonly payload: ISignedTransactionPayload; + readonly header?: string; +} + +const SignedTransactionPayload: React.FC = ( + { payload, header } +) => { + const { inputs, outputs } = payload.transaction; + const transactionId = Utils.transactionId(payload) + + return ( +
+ {header && ( +
+
+

{header}

+ +
+
+ )} +
+
+
+

From

+ + {inputs.length} +
+
+ {/* {inputs.map((input, idx) => )} */} + +
+
+ +
+
+

To

+ + {outputs.length} +
+
+ {outputs.map((output, idx) => ( + + ))} +
+
+
+
+ ); +}; + +SignedTransactionPayload.defaultProps = { + payload: undefined, + header: undefined, +}; + +export default SignedTransactionPayload; + diff --git a/client/src/app/components/nova/block/payload/TaggedDataPayload.tsx b/client/src/app/components/nova/block/payload/TaggedDataPayload.tsx new file mode 100644 index 000000000..8c893dc36 --- /dev/null +++ b/client/src/app/components/nova/block/payload/TaggedDataPayload.tsx @@ -0,0 +1,49 @@ +import { TaggedDataPayload as ITaggedDataPayload} from "@iota/sdk-wasm-nova/web"; +import React from "react"; +import DataToggle from "~/app/components/DataToggle"; + +interface TaggedDataPayloadProps { + readonly payload: ITaggedDataPayload; +} + +const TaggedDataPayload: React.FC = ( + { payload } +) => { + const { tag, data } = payload; + + return ( +
+
+ {tag && ( + +
+ Tag +
+ +
+ )} + {data && ( + +
+ Data +
+ +
+ )} +
+
+ ); +}; + +TaggedDataPayload.defaultProps = { + payload: undefined, +}; + +export default TaggedDataPayload; + diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index 2a97e4674..6b8561b4a 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -35,7 +35,7 @@ const Block: React.FC> = ( switch (block?.body?.type) { case BlockBodyType.Basic: { pageTitle = `Basic ${pageTitle}`; - blockBody = blockBody as BasicBlockBody + blockBody = block?.body as BasicBlockBody break; } case BlockBodyType.Validation: { @@ -47,7 +47,6 @@ const Block: React.FC> = ( break; } } - const blockContent = block ? ( diff --git a/client/src/helpers/nova/nameHelper.ts b/client/src/helpers/nova/nameHelper.ts new file mode 100644 index 000000000..a9f294558 --- /dev/null +++ b/client/src/helpers/nova/nameHelper.ts @@ -0,0 +1,37 @@ +import { UnlockType } from "@iota/sdk-wasm-nova/web"; + +export class NameHelper { + /** + * Get the name for the unlock type. + * @param type The type to get the name for. + * @returns The unlock type name. + */ + public static getUnlockTypeName(type: number): string { + switch (type) { + case UnlockType.Signature: { + return "Signature Unlock"; + } + case UnlockType.Reference: { + return "Reference Unlock"; + } + case UnlockType.Account: { + return "Account Unlock"; + } + case UnlockType.Anchor: { + return "Anchor Unlock"; + } + case UnlockType.Nft: { + return "NFT Unlock"; + } + case UnlockType.Multi: { + return "Multi Unlock"; + } + case UnlockType.Empty: { + return "Empty Unlock"; + } + default: { + return "Unknown Unlock"; + } + } + } +} From 72f049ee653bf2d15626dc8a7206c480aed6c551 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Fri, 12 Jan 2024 17:01:11 +0100 Subject: [PATCH 06/15] fix: add block payload section and transaction helper --- client/src/app/components/nova/Input.tsx | 97 ++++++++ .../payload/SignedTransactionPayload.tsx | 10 +- .../block/section/BlockPayloadSection.tsx | 78 ++++++ client/src/app/routes/nova/Block.tsx | 79 +++++- .../helpers/nova/hooks/useInputsAndOutputs.ts | 59 +++++ client/src/helpers/nova/transactionsHelper.ts | 229 ++++++++++++++++++ client/src/models/api/nova/IInput.ts | 34 +++ client/src/models/api/nova/IOutput.ts | 26 ++ 8 files changed, 601 insertions(+), 11 deletions(-) create mode 100644 client/src/app/components/nova/Input.tsx create mode 100644 client/src/app/components/nova/block/section/BlockPayloadSection.tsx create mode 100644 client/src/helpers/nova/hooks/useInputsAndOutputs.ts create mode 100644 client/src/helpers/nova/transactionsHelper.ts create mode 100644 client/src/models/api/nova/IInput.ts create mode 100644 client/src/models/api/nova/IOutput.ts diff --git a/client/src/app/components/nova/Input.tsx b/client/src/app/components/nova/Input.tsx new file mode 100644 index 000000000..399f557e9 --- /dev/null +++ b/client/src/app/components/nova/Input.tsx @@ -0,0 +1,97 @@ +/* eslint-disable jsdoc/require-param */ +/* eslint-disable jsdoc/require-returns */ +import { Utils } from "@iota/sdk-wasm-nova/web"; +import classNames from "classnames"; +import React, { useContext, useState } from "react"; +import { useHistory, Link } from "react-router-dom"; +import Bech32Address from "../stardust/address/Bech32Address"; +import OutputView from "./OutputView"; +import DropdownIcon from "~assets/dropdown-arrow.svg?react"; +import { formatAmount } from "~helpers/stardust/valueFormatHelper"; +import { IInput } from "~models/api/nova/IInput"; +import NetworkContext from "../../context/NetworkContext"; + +interface InputProps { + /** + * The inputs. + */ + readonly input: IInput; + /** + * The network in context. + */ + readonly network: string; +} + +/** + * Component which will display an Input on stardust. + */ +const Input: React.FC = ({ input, network }) => { + const history = useHistory(); + const { tokenInfo } = useContext(NetworkContext); + const [isExpanded, setIsExpanded] = useState(false); + const [isFormattedBalance, setIsFormattedBalance] = useState(true); + + const fallbackInputView = ( + +
setIsExpanded(!isExpanded)} + > +
+ +
+
+ +
+ {input.amount && ( + { + setIsFormattedBalance(!isFormattedBalance); + e.stopPropagation(); + }} + className="card--value amount-size pointer" + > + {formatAmount(input.amount, tokenInfo, !isFormattedBalance)} + + )} +
+ + {isExpanded && ( + +
Address
+
+ +
+
Transaction Id
+
+ + {input.transactionId} + +
+
Transaction Output Index
+
{input.transactionInputIndex}
+
)} +
+ ); + + const outputId = Utils.computeOutputId( + input.transactionId, input.transactionInputIndex + ); + + return input.output ? + : fallbackInputView; +}; + +export default Input; diff --git a/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx b/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx index 284eebf8e..5e93415bd 100644 --- a/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx +++ b/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx @@ -4,16 +4,19 @@ import Modal from "~/app/components/Modal"; import Unlocks from "~/app/components/nova/Unlocks"; import OutputView from "~/app/components/nova/OutputView"; import transactionPayloadMessage from "~assets/modals/stardust/block/transaction-payload.json"; +import { IInput } from "~/models/api/nova/IInput"; +import Input from "~/app/components/nova/Input"; interface SignedTransactionPayloadProps { readonly payload: ISignedTransactionPayload; + readonly inputs: IInput[]; readonly header?: string; } const SignedTransactionPayload: React.FC = ( - { payload, header } + { payload, inputs, header } ) => { - const { inputs, outputs } = payload.transaction; + const { networkId, outputs } = payload.transaction; const transactionId = Utils.transactionId(payload) return ( @@ -34,7 +37,7 @@ const SignedTransactionPayload: React.FC = ( {inputs.length}
- {/* {inputs.map((input, idx) => )} */} + {inputs.map((input, idx) => )}
@@ -63,6 +66,7 @@ const SignedTransactionPayload: React.FC = ( SignedTransactionPayload.defaultProps = { payload: undefined, + inputs: undefined, header: undefined, }; diff --git a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx new file mode 100644 index 000000000..0c12dc1c5 --- /dev/null +++ b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx @@ -0,0 +1,78 @@ +import { + Block, PayloadType, SignedTransactionPayload as ISignedTransactionPayload, + TaggedDataPayload as ITaggedDataPayload +} from "@iota/sdk-wasm-nova/web"; +import React from "react"; +import { IInput } from "~models/api/nova/IInput"; +import { IOutput } from "~models/api/nova/IOutput"; +import TaggedDataPayload from "../payload/TaggedDataPayload"; +import SignedTransactionPayload from "../payload/SignedTransactionPayload"; + +interface BlockPayloadSectionProps { + readonly block: Block; + readonly inputs?: IInput[]; + readonly outputs?: IOutput[]; + readonly transferTotal?: number; +} + +const BlockPayloadSection: React.FC = ( + { block, inputs, outputs, transferTotal } +) => { + const payload = block.body?.asBasic().payload + if ( + payload?.type === PayloadType.SignedTransaction && + inputs && outputs && transferTotal !== undefined + ) { + const transactionPayload = payload as ISignedTransactionPayload; + const transaction = transactionPayload.transaction; + + return ( + +
+ +
+ { + transaction.payload?.type === PayloadType.TaggedData && +
+ +
+ } +
+ ); + } else if ( + payload?.type === PayloadType.CandidacyAnnouncement + ) { + return ( +
+ {/* todo */} + CandidacyAnnouncement +
+ ); + } else if ( + payload?.type === PayloadType.TaggedData + ) { + return ( +
+ +
+ ); + } + + return null; +}; + +BlockPayloadSection.defaultProps = { + inputs: undefined, + outputs: undefined, + transferTotal: undefined, +}; + +export default BlockPayloadSection; + diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index 6b8561b4a..54ada80b1 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -1,17 +1,23 @@ -import React from "react"; +import React, { useContext, useState } from "react"; import { RouteComponentProps } from "react-router-dom"; import mainHeaderMessage from "~assets/modals/stardust/block/main-header.json"; import { useBlock } from "~helpers/nova/hooks/useBlock"; import NotFound from "../../components/NotFound"; -import { BasicBlockBody, BlockBodyType, ValidationBlockBody } from "@iota/sdk-wasm-nova/web"; +import { BasicBlockBody, BlockBodyType, PayloadType, ValidationBlockBody } from "@iota/sdk-wasm-nova/web"; import Modal from "~/app/components/Modal"; import Spinner from "~/app/components/Spinner"; import TruncatedId from "~/app/components/stardust/TruncatedId"; import { DateHelper } from "~/helpers/dateHelper"; import MilestoneSignaturesSection from "~/app/components/stardust/block/payload/milestone/MilestoneSignaturesSection"; import { Ed25519Signature } from "@iota/sdk-wasm/web"; - +import { useInputsAndOutputs } from "~/helpers/nova/hooks/useInputsAndOutputs"; +import BlockPayloadSection from "~/app/components/nova/block/section/BlockPayloadSection"; +import { formatAmount } from "~/helpers/stardust/valueFormatHelper"; +import NetworkContext from "~/app/context/NetworkContext"; +import TabbedSection from "~/app/components/hoc/TabbedSection"; +import taggedDataPayloadInfo from "~assets/modals/stardust/block/tagged-data-payload.json"; +import transactionPayloadInfo from "~assets/modals/stardust/block/transaction-payload.json"; export interface BlockProps { /** * The network to lookup. @@ -27,8 +33,10 @@ export interface BlockProps { const Block: React.FC> = ( { history, match: { params: { network, blockId } } } ) => { - + const { tokenInfo } = useContext(NetworkContext); + const [isFormattedBalance, setIsFormattedBalance] = useState(true); const [block, isLoading, blockError] = useBlock(network, blockId); + const [inputs, outputs, transferTotal] = useInputsAndOutputs(network, block); let blockBody: BasicBlockBody | ValidationBlockBody | undefined let pageTitle = "Block"; @@ -48,6 +56,20 @@ const Block: React.FC> = ( } } + const tabbedSections = []; + let idx = 0; + if (block) { + tabbedSections.push( + + ); + } + const blockContent = block ? (
@@ -129,11 +151,52 @@ const Block: React.FC> = (
)}
- - {block.body?.type === BlockBodyType.Basic && ( + {blockBody?.type === BlockBodyType.Basic && (
- {/* todo: add all block payload */} - {JSON.stringify(blockBody)} + {blockBody?.asBasic().payload?.type === PayloadType.SignedTransaction && + transferTotal !== null && ( +
+
+ Amount transacted +
+
+ setIsFormattedBalance(!isFormattedBalance)} + className="pointer margin-r-5" + > + {formatAmount( + transferTotal, + tokenInfo, + !isFormattedBalance + )} + +
+
+ )} + + + {tabbedSections} +
)} diff --git a/client/src/helpers/nova/hooks/useInputsAndOutputs.ts b/client/src/helpers/nova/hooks/useInputsAndOutputs.ts new file mode 100644 index 000000000..255577ea3 --- /dev/null +++ b/client/src/helpers/nova/hooks/useInputsAndOutputs.ts @@ -0,0 +1,59 @@ +import { Block, PayloadType } from "@iota/sdk-wasm-nova/web"; +import { useContext, useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import NetworkContext from "~app/context/NetworkContext"; +import { ServiceFactory } from "~factories/serviceFactory"; +import { IInput } from "~models/api/nova/IInput"; +import { IOutput } from "~models/api/nova/IOutput"; +import { STARDUST } from "~models/config/protocolVersion"; +import { NovaApiClient } from "~services/nova/novaApiClient"; +import { TransactionsHelper } from "../transactionsHelper"; + +/** + * Fetch block inputs and outputs + * @param network The Network in context + * @param block The block + * @returns The inputs, unlocks, outputs, transfer total an a loading bool. + */ +export function useInputsAndOutputs(network: string, block: Block | null): + [ + IInput[] | null, + IOutput[] | null, + number | null, + boolean + ] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${STARDUST}`)); + const { bech32Hrp } = useContext(NetworkContext); + const [tsxInputs, setInputs] = useState(null); + const [tsxOutputs, setOutputs] = useState(null); + const [tsxTransferTotal, setTransferTotal] = useState(null); + + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (block?.body.asBasic().payload?.type === PayloadType.SignedTransaction) { + // eslint-disable-next-line no-void + void (async () => { + const { inputs, outputs, transferTotal } = + await TransactionsHelper.getInputsAndOutputs( + block, + network, + bech32Hrp, + apiClient + ); + if (isMounted) { + setInputs(inputs); + setOutputs(outputs); + setTransferTotal(transferTotal); + setIsLoading(false); + } + })(); + } else { + setIsLoading(false); + } + }, [network, block]); + + return [tsxInputs, tsxOutputs, tsxTransferTotal, isLoading]; +} diff --git a/client/src/helpers/nova/transactionsHelper.ts b/client/src/helpers/nova/transactionsHelper.ts new file mode 100644 index 000000000..0492e9a3b --- /dev/null +++ b/client/src/helpers/nova/transactionsHelper.ts @@ -0,0 +1,229 @@ +import { + AddressUnlockCondition, + Block, BlockBodyType, CommonOutput, DelegationOutput, GovernorAddressUnlockCondition, + ImmutableAccountAddressUnlockCondition, + InputType,OutputType, PayloadType, + SignatureUnlock, + SignedTransactionPayload, + StateControllerAddressUnlockCondition, + UnlockCondition, UnlockConditionType, UnlockType, Utils, UTXOInput +} from "@iota/sdk-wasm-nova/web"; +import { IBech32AddressDetails } from "~/models/api/IBech32AddressDetails"; +import { IInput } from "~/models/api/nova/IInput"; +import { IOutput } from "~/models/api/nova/IOutput"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; +import { Bech32AddressHelper } from "../stardust/bech32AddressHelper"; +import { Converter } from "../stardust/convertUtils"; + +interface TransactionInputsAndOutputsResponse { + inputs: IInput[]; + outputs: IOutput[]; + unlockAddresses: IBech32AddressDetails[]; + transferTotal: number; +} + +export class TransactionsHelper { + public static async getInputsAndOutputs(block: Block | undefined, network: string, + _bechHrp: string, apiClient: NovaApiClient + ): Promise { + const GENESIS_HASH = "0".repeat(64); + const inputs: IInput[] = []; + const outputs: IOutput[] = []; + const remainderOutputs: IOutput[] = []; + const unlockAddresses: IBech32AddressDetails[] = []; + let transferTotal = 0; + let sortedOutputs: IOutput[] = []; + + if (block?.body.type === BlockBodyType.Basic && block?.body.asBasic().payload?.type === PayloadType.SignedTransaction) { + const payload: SignedTransactionPayload = block?.body.asBasic().payload as SignedTransactionPayload; + const transactionId = Utils.transactionId(payload); + + // Unlocks + const unlocks = payload.unlocks; + + // unlock Addresses computed from public keys in unlocks + for (let i = 0; i < unlocks.length; i++) { + const unlock = unlocks[i]; + let signatureUnlock: SignatureUnlock | undefined; + + if (unlock.type === UnlockType.Signature) { + signatureUnlock = unlock as SignatureUnlock; + } + else { + let refUnlockIdx = i; + // unlock references can be transitive, + // so we need to follow the path until we find the signature + do { + const referencedUnlock = unlocks[refUnlockIdx] + + if (referencedUnlock.type === UnlockType.Signature) { + signatureUnlock = referencedUnlock as SignatureUnlock + } else if (referencedUnlock.type === UnlockType.Multi || referencedUnlock.type === UnlockType.Empty) { + break; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + refUnlockIdx = (referencedUnlock as any).reference; + } + + } while (!signatureUnlock); + } + + if (signatureUnlock) { + unlockAddresses.push( + Bech32AddressHelper.buildAddress( + _bechHrp, + Utils.hexPublicKeyToBech32Address(signatureUnlock.signature.publicKey, _bechHrp) + ) + ); + } + } + + const transaction = payload.transaction; + + // Inputs + for (let i = 0; i < transaction.inputs.length; i++) { + let outputDetails; + let amount; + let isGenesis = false; + const address = unlockAddresses[i]; + const input = transaction.inputs[i]; + + if (input.type === InputType.UTXO) { + const utxoInput = input as UTXOInput; + isGenesis = utxoInput.transactionId === GENESIS_HASH; + + const outputId = Utils.computeOutputId( + utxoInput.transactionId, + utxoInput.transactionOutputIndex + ); + + const response = await apiClient.outputDetails({ network, outputId }); + const details = response.output; + + if (!response.error && details?.output && details?.metadata) { + outputDetails = { + output: details.output, + metadata: details.metadata + }; + amount = Number(details.output.amount); + } + + inputs.push({ + ...utxoInput, + // TODO-sdk Rename the field + transactionInputIndex: utxoInput.transactionOutputIndex, + amount, + isGenesis, + outputId, + output: outputDetails, + address + }); + } + } + + // Outputs + for (let i = 0; i < transaction.outputs.length; i++) { + const outputId = Utils.computeOutputId(transactionId, i); + + if (transaction.outputs[i].type === OutputType.Delegation) { + const output = transaction.outputs[i] as DelegationOutput; + + outputs.push({ + id: outputId, + output, + amount: Number(transaction.outputs[i].amount) + }); + } else { + const output = transaction.outputs[i] as CommonOutput; + + const address: IBech32AddressDetails = TransactionsHelper.bechAddressFromAddressUnlockCondition( + output.unlockConditions, _bechHrp, output.type + ); + + const isRemainder = inputs.some(input => input.address.bech32 === address.bech32); + + if (isRemainder) { + remainderOutputs.push({ + id: outputId, + address, + amount: Number(transaction.outputs[i].amount), + isRemainder, + output + }); + } else { + outputs.push({ + id: outputId, + address, + amount: Number(transaction.outputs[i].amount), + isRemainder, + output + }); + } + + if (!isRemainder) { + transferTotal += Number(transaction.outputs[i].amount); + } + } + } + + sortedOutputs = [...outputs, ...remainderOutputs]; + this.sortInputsAndOuputsByIndex(sortedOutputs); + this.sortInputsAndOuputsByIndex(inputs); + } + + return { inputs, outputs: sortedOutputs, unlockAddresses, transferTotal }; + } + + + /** + * Sort inputs and outputs in assending order by index. + * @param items Inputs or Outputs. + */ + public static sortInputsAndOuputsByIndex(items: IInput[] | IOutput[]) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + items.sort((a: any, b: any) => { + const firstIndex: string = a.id ? a.id.slice(-4) : a.outputId.slice(-4); + const secondIndex: string = b.id ? b.id.slice(-4) : b.outputId.slice(-4); + const firstFormattedIndex = Converter.convertToBigEndian(firstIndex); + const secondFormattedIndex = Converter.convertToBigEndian(secondIndex); + + return Number.parseInt(firstFormattedIndex, 16) - Number.parseInt(secondFormattedIndex, 16); + }); + } + + private static bechAddressFromAddressUnlockCondition( + unlockConditions: UnlockCondition[], + _bechHrp: string, + outputType: number + ): IBech32AddressDetails { + let address: IBech32AddressDetails = { bech32: "" }; + let unlockCondition; + + if (outputType === OutputType.Basic || outputType === OutputType.Nft) { + unlockCondition = unlockConditions?.filter( + ot => ot.type === UnlockConditionType.Address + ).map(ot => ot as AddressUnlockCondition)[0]; + } else if (outputType === OutputType.Account) { + if (unlockConditions.some(ot => ot.type === UnlockConditionType.StateControllerAddress)) { + unlockCondition = unlockConditions?.filter( + ot => ot.type === UnlockConditionType.StateControllerAddress + ).map(ot => ot as StateControllerAddressUnlockCondition)[0]; + } + if (unlockConditions.some(ot => ot.type === UnlockConditionType.GovernorAddress)) { + unlockCondition = unlockConditions?.filter( + ot => ot.type === UnlockConditionType.GovernorAddress + ).map(ot => ot as GovernorAddressUnlockCondition)[0]; + } + } else if (outputType === OutputType.Foundry) { + unlockCondition = unlockConditions?.filter( + ot => ot.type === UnlockConditionType.ImmutableAccountAddress + ).map(ot => ot as ImmutableAccountAddressUnlockCondition)[0]; + } + + if (unlockCondition?.address) { + address = { bech32: unlockCondition?.address.toString()}; + } + + return address; + } +} \ No newline at end of file diff --git a/client/src/models/api/nova/IInput.ts b/client/src/models/api/nova/IInput.ts new file mode 100644 index 000000000..42e86006e --- /dev/null +++ b/client/src/models/api/nova/IInput.ts @@ -0,0 +1,34 @@ +import { HexEncodedString, OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { IBech32AddressDetails } from "../IBech32AddressDetails"; + +export interface IInput { + /** + * The transaction Id. + */ + transactionId: HexEncodedString; + /** + * The input index. + */ + transactionInputIndex: number; + /** + * The output id. + */ + outputId: string; + /** + * The output used as input. + */ + output?: OutputResponse; + /** + * The transaction address details. + */ + address: IBech32AddressDetails; + /** + * The amount. + */ + amount?: number; + /** + * The is genesis flag. + */ + isGenesis: boolean; +} + diff --git a/client/src/models/api/nova/IOutput.ts b/client/src/models/api/nova/IOutput.ts new file mode 100644 index 000000000..47765b354 --- /dev/null +++ b/client/src/models/api/nova/IOutput.ts @@ -0,0 +1,26 @@ +import { Output } from "@iota/sdk-wasm-nova/web"; +import { IBech32AddressDetails } from "../IBech32AddressDetails"; + +export interface IOutput { + /** + * The output id. + */ + id: string; + /** + * The Bech32 address details. + */ + address?: IBech32AddressDetails; + /** + * The output. + */ + output: Output; + /** + * The output amount. + */ + amount: number; + /** + * Is remainder output flag. + */ + isRemainder?: boolean; +} + From a25d688ef1f71b3e3b412c03352fc43c30a672a5 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Mon, 15 Jan 2024 16:29:38 +0100 Subject: [PATCH 07/15] feat: add block transaction metadata tab --- api/package-lock.json | 4 +- .../models/api/nova/IBlockDetailsResponse.ts | 9 +++ api/src/routes.ts | 3 +- api/src/routes/nova/block/metadata/get.ts | 32 +++++++++ api/src/services/nova/novaApi.ts | 24 ++++++- client/package-lock.json | 2 +- .../block/section/BlockPayloadSection.tsx | 5 +- .../section/TransactionMetadataSection.tsx | 62 +++++++++++++++++ client/src/app/routes/nova/Block.tsx | 45 ++++++++---- client/src/helpers/nova/hooks/useBlock.ts | 1 - .../helpers/nova/hooks/useBlockMetadata.ts | 68 +++++++++++++++++++ .../helpers/nova/hooks/useInputsAndOutputs.ts | 12 ++-- client/src/helpers/nova/transactionsHelper.ts | 6 +- .../api/nova/block/IBlockDetailsRequest.ts | 11 +++ .../api/nova/block/IBlockDetailsResponse.ts | 9 +++ .../models/api/nova/block/IBlockMetadata.ts | 14 ++++ client/src/services/nova/novaApiClient.ts | 13 ++++ setup_nova.sh | 2 +- 18 files changed, 293 insertions(+), 29 deletions(-) create mode 100644 api/src/models/api/nova/IBlockDetailsResponse.ts create mode 100644 api/src/routes/nova/block/metadata/get.ts create mode 100644 client/src/app/components/nova/block/section/TransactionMetadataSection.tsx create mode 100644 client/src/helpers/nova/hooks/useBlockMetadata.ts create mode 100644 client/src/models/api/nova/block/IBlockDetailsRequest.ts create mode 100644 client/src/models/api/nova/block/IBlockDetailsResponse.ts create mode 100644 client/src/models/api/nova/block/IBlockMetadata.ts diff --git a/api/package-lock.json b/api/package-lock.json index 6fc4f6198..46280d5fc 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -64,10 +64,11 @@ }, "../iota-sdk/bindings/nodejs": { "name": "@iota/sdk-nova", - "version": "1.1.3", + "version": "1.1.4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { + "@babel/traverse": "^7.23.2", "@types/node": "^18.15.12", "class-transformer": "^0.5.1", "prebuild-install": "^7.1.1", @@ -11313,6 +11314,7 @@ "@iota/sdk-nova": { "version": "file:../iota-sdk/bindings/nodejs", "requires": { + "@babel/traverse": "^7.23.2", "@napi-rs/cli": "^1.0.0", "@types/jest": "^29.4.0", "@types/node": "^18.15.12", diff --git a/api/src/models/api/nova/IBlockDetailsResponse.ts b/api/src/models/api/nova/IBlockDetailsResponse.ts new file mode 100644 index 000000000..298703267 --- /dev/null +++ b/api/src/models/api/nova/IBlockDetailsResponse.ts @@ -0,0 +1,9 @@ +import { IBlockMetadata } from "@iota/sdk-nova"; +import { IResponse } from "../IResponse"; + +export interface IBlockDetailsResponse extends IResponse { + /** + * Block metadata. + */ + metadata?: IBlockMetadata; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index 5b28d88c5..4fb9b19ff 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -145,5 +145,6 @@ export const routes: IRoute[] = [ }, // Nova { path: "/nova/output/:network/:outputId", method: "get", folder: "nova/output", func: "get" }, - { path: "/nova/block/:network/:blockId", method: "get", folder: "nova/block", 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/block/metadata/get.ts b/api/src/routes/nova/block/metadata/get.ts new file mode 100644 index 000000000..2850ceb93 --- /dev/null +++ b/api/src/routes/nova/block/metadata/get.ts @@ -0,0 +1,32 @@ +import { ServiceFactory } from "../../../../factories/serviceFactory"; +import { IBlockDetailsResponse } from "../../../../models/api/nova/IBlockDetailsResponse"; +import { IBlockRequest } from "../../../../models/api/stardust/IBlockRequest"; +import { IConfiguration } from "../../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../../models/db/protocolVersion"; +import { NetworkService } from "../../../../services/networkService"; +import { NovaApi } from "../../../../services/nova/novaApi"; +import { ValidationHelper } from "../../../../utils/validationHelper"; + +/** + * Fetch the block details from the network. + * @param _ The configuration. + * @param request The request. + * @returns The response. + */ +export async function get( + _: IConfiguration, + request: IBlockRequest +): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.blockId, "blockId"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + return NovaApi.blockDetails(networkConfig, request.blockId); +} diff --git a/api/src/services/nova/novaApi.ts b/api/src/services/nova/novaApi.ts index c1184c107..13c663273 100644 --- a/api/src/services/nova/novaApi.ts +++ b/api/src/services/nova/novaApi.ts @@ -1,8 +1,9 @@ import { - __ClientMethods__, OutputResponse, Client, Block + __ClientMethods__, OutputResponse, Client, Block, IBlockMetadata } from "@iota/sdk-nova"; import { ServiceFactory } from "../../factories/serviceFactory"; import logger from "../../logger"; +import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse"; import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; import { INetwork } from "../../models/db/INetwork"; @@ -45,6 +46,27 @@ export class NovaApi { } } + /** + * Get the block details. + * @param network The network to find the items on. + * @param blockId The block id to get the details. + * @returns The item details. + */ + public static async blockDetails(network: INetwork, blockId: string): Promise { + blockId = HexHelper.addPrefix(blockId); + const metadata = await this.tryFetchNodeThenPermanode( + blockId, + "getBlockMetadata", + network + ); + + if (metadata) { + return { + metadata + }; + } + } + /** * Get the output details. * @param network The network to find the items on. diff --git a/client/package-lock.json b/client/package-lock.json index cc8e515b3..2ba6e335f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -102,7 +102,7 @@ }, "../iota-sdk/bindings/wasm": { "name": "@iota/sdk-wasm-nova", - "version": "1.1.1", + "version": "1.1.2", "license": "Apache-2.0", "dependencies": { "class-transformer": "^0.5.1", diff --git a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx index 0c12dc1c5..374de777d 100644 --- a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx +++ b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx @@ -1,6 +1,7 @@ import { Block, PayloadType, SignedTransactionPayload as ISignedTransactionPayload, - TaggedDataPayload as ITaggedDataPayload + TaggedDataPayload as ITaggedDataPayload, + BasicBlockBody } from "@iota/sdk-wasm-nova/web"; import React from "react"; import { IInput } from "~models/api/nova/IInput"; @@ -18,7 +19,7 @@ interface BlockPayloadSectionProps { const BlockPayloadSection: React.FC = ( { block, inputs, outputs, transferTotal } ) => { - const payload = block.body?.asBasic().payload + const payload = (block.body as BasicBlockBody).payload if ( payload?.type === PayloadType.SignedTransaction && inputs && outputs && transferTotal !== undefined diff --git a/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx b/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx new file mode 100644 index 000000000..8ebfd18bd --- /dev/null +++ b/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx @@ -0,0 +1,62 @@ +import { TransactionMetadata } from "@iota/sdk-wasm-nova/web"; +import React from "react"; +import Spinner from "../../../Spinner"; +import TruncatedId from "~/app/components/stardust/TruncatedId"; + +interface TransactionMetadataSectionProps { + readonly network: string; + readonly metadata?: TransactionMetadata; + readonly metadataError?: string; + readonly isLinksDisabled: boolean; +} + +const TransactionMetadataSection: React.FC = ( + { network, metadata, metadataError, isLinksDisabled } +) => ( +
+
+ {!metadata && !metadataError && ()} + {metadataError && ( +

Failed to retrieve metadata. {metadataError}

+ )} + {metadata && !metadataError && ( + +
+
+ Transaction Id +
+
+ +
+
+
+
+ Transaction Status +
+
+ {metadata.transactionState} +
+
+ {metadata.transactionFailureReason && ( +
+
Failure Reason
+
{metadata.transactionFailureReason}
+
+ )} +
+ )} +
+
+); + +TransactionMetadataSection.defaultProps = { + metadata: undefined, + metadataError: undefined +}; + +export default TransactionMetadataSection; + diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index 54ada80b1..96cde4611 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -1,7 +1,8 @@ -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; import { RouteComponentProps } from "react-router-dom"; import mainHeaderMessage from "~assets/modals/stardust/block/main-header.json"; +import metadataInfo from "~assets/modals/stardust/block/metadata.json"; import { useBlock } from "~helpers/nova/hooks/useBlock"; import NotFound from "../../components/NotFound"; import { BasicBlockBody, BlockBodyType, PayloadType, ValidationBlockBody } from "@iota/sdk-wasm-nova/web"; @@ -14,10 +15,12 @@ import { Ed25519Signature } from "@iota/sdk-wasm/web"; import { useInputsAndOutputs } from "~/helpers/nova/hooks/useInputsAndOutputs"; import BlockPayloadSection from "~/app/components/nova/block/section/BlockPayloadSection"; import { formatAmount } from "~/helpers/stardust/valueFormatHelper"; -import NetworkContext from "~/app/context/NetworkContext"; +import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; import TabbedSection from "~/app/components/hoc/TabbedSection"; import taggedDataPayloadInfo from "~assets/modals/stardust/block/tagged-data-payload.json"; import transactionPayloadInfo from "~assets/modals/stardust/block/transaction-payload.json"; +import { useBlockMetadata } from "~/helpers/nova/hooks/useBlockMetadata"; +import TransactionMetadataSection from "~/app/components/nova/block/section/TransactionMetadataSection"; export interface BlockProps { /** * The network to lookup. @@ -33,13 +36,18 @@ export interface BlockProps { const Block: React.FC> = ( { history, match: { params: { network, blockId } } } ) => { - const { tokenInfo } = useContext(NetworkContext); + const { networkInfo } = useNetworkInfoNova(); const [isFormattedBalance, setIsFormattedBalance] = useState(true); const [block, isLoading, blockError] = useBlock(network, blockId); + const [blockMetadata] = useBlockMetadata(network, blockId); const [inputs, outputs, transferTotal] = useInputsAndOutputs(network, block); + function isBasicBlockBody(body: BasicBlockBody | ValidationBlockBody): body is BasicBlockBody { + return body.type === BlockBodyType.Basic; + } let blockBody: BasicBlockBody | ValidationBlockBody | undefined let pageTitle = "Block"; + switch (block?.body?.type) { case BlockBodyType.Basic: { pageTitle = `Basic ${pageTitle}`; @@ -70,6 +78,18 @@ const Block: React.FC> = ( ); } + if (blockMetadata.metadata?.transactionMetadata) { + tabbedSections.push( + + ); + } + + const blockContent = block ? (
@@ -151,9 +171,9 @@ const Block: React.FC> = (
)}
- {blockBody?.type === BlockBodyType.Basic && ( + {blockBody && isBasicBlockBody(blockBody) && (
- {blockBody?.asBasic().payload?.type === PayloadType.SignedTransaction && + {blockBody.payload?.type === PayloadType.SignedTransaction && transferTotal !== null && (
@@ -166,7 +186,7 @@ const Block: React.FC> = ( > {formatAmount( transferTotal, - tokenInfo, + networkInfo.tokenInfo, !isFormattedBalance )} @@ -177,19 +197,20 @@ const Block: React.FC> = ( (true); useEffect(() => { - console.log("useBlock nova") setIsLoading(true); setBlock(null); if (blockId) { diff --git a/client/src/helpers/nova/hooks/useBlockMetadata.ts b/client/src/helpers/nova/hooks/useBlockMetadata.ts new file mode 100644 index 000000000..7fe23bdcc --- /dev/null +++ b/client/src/helpers/nova/hooks/useBlockMetadata.ts @@ -0,0 +1,68 @@ +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"; +import { HexHelper } from "~/helpers/stardust/hexHelper"; +import { IBlockMetadata } from "~/models/api/nova/block/IBlockMetadata"; + +/** + * Fetch the block metadata + * @param network The Network in context + * @param blockId The block id + * @returns The block metadata and loading bool. + */ +export function useBlockMetadata(network: string, blockId: string | null): + [ + IBlockMetadata, + boolean + ] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [blockMetadata, setBlockMetadata] = useState({ metadata: { blockId: blockId ?? '', blockState: "pending"} }); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let timerId: NodeJS.Timeout | undefined; + setIsLoading(true); + if (blockId) { + setBlockMetadata({ metadata: { blockId, blockState: "pending"} }); + const fetchMetadata = async () => { + try { + const details = await apiClient.blockDetails({ + network, + blockId: HexHelper.addPrefix(blockId) + }); + + if (isMounted) { + setBlockMetadata({metadata: details?.metadata}); + + if (!details?.metadata) { + timerId = setTimeout(async () => { + await fetchMetadata(); + }, 10000); + } + } + } catch (error) { + if (error instanceof Error && isMounted) { + setBlockMetadata({ metadataError: error.message}); + } + } finally { + setIsLoading(false); + } + }; + // eslint-disable-next-line no-void + void fetchMetadata(); + } else { + setIsLoading(false); + } + + return () => { + if (timerId) { + clearTimeout(timerId); + } + }; + }, [network, blockId]); + + return [blockMetadata, isLoading]; +} diff --git a/client/src/helpers/nova/hooks/useInputsAndOutputs.ts b/client/src/helpers/nova/hooks/useInputsAndOutputs.ts index 255577ea3..c655c6072 100644 --- a/client/src/helpers/nova/hooks/useInputsAndOutputs.ts +++ b/client/src/helpers/nova/hooks/useInputsAndOutputs.ts @@ -1,13 +1,13 @@ -import { Block, PayloadType } from "@iota/sdk-wasm-nova/web"; -import { useContext, useEffect, useState } from "react"; +import { BasicBlockBody, Block, PayloadType } from "@iota/sdk-wasm-nova/web"; +import { useEffect, useState } from "react"; import { useIsMounted } from "~helpers/hooks/useIsMounted"; -import NetworkContext from "~app/context/NetworkContext"; import { ServiceFactory } from "~factories/serviceFactory"; import { IInput } from "~models/api/nova/IInput"; import { IOutput } from "~models/api/nova/IOutput"; import { STARDUST } from "~models/config/protocolVersion"; import { NovaApiClient } from "~services/nova/novaApiClient"; import { TransactionsHelper } from "../transactionsHelper"; +import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; /** * Fetch block inputs and outputs @@ -24,7 +24,7 @@ export function useInputsAndOutputs(network: string, block: Block | null): ] { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${STARDUST}`)); - const { bech32Hrp } = useContext(NetworkContext); + const { networkInfo } = useNetworkInfoNova(); const [tsxInputs, setInputs] = useState(null); const [tsxOutputs, setOutputs] = useState(null); const [tsxTransferTotal, setTransferTotal] = useState(null); @@ -33,14 +33,14 @@ export function useInputsAndOutputs(network: string, block: Block | null): useEffect(() => { setIsLoading(true); - if (block?.body.asBasic().payload?.type === PayloadType.SignedTransaction) { + if (block && (block?.body as BasicBlockBody).payload?.type === PayloadType.SignedTransaction) { // eslint-disable-next-line no-void void (async () => { const { inputs, outputs, transferTotal } = await TransactionsHelper.getInputsAndOutputs( block, network, - bech32Hrp, + networkInfo.bech32Hrp, apiClient ); if (isMounted) { diff --git a/client/src/helpers/nova/transactionsHelper.ts b/client/src/helpers/nova/transactionsHelper.ts index 0492e9a3b..7c98fa656 100644 --- a/client/src/helpers/nova/transactionsHelper.ts +++ b/client/src/helpers/nova/transactionsHelper.ts @@ -1,5 +1,6 @@ import { AddressUnlockCondition, + BasicBlockBody, Block, BlockBodyType, CommonOutput, DelegationOutput, GovernorAddressUnlockCondition, ImmutableAccountAddressUnlockCondition, InputType,OutputType, PayloadType, @@ -34,8 +35,8 @@ export class TransactionsHelper { let transferTotal = 0; let sortedOutputs: IOutput[] = []; - if (block?.body.type === BlockBodyType.Basic && block?.body.asBasic().payload?.type === PayloadType.SignedTransaction) { - const payload: SignedTransactionPayload = block?.body.asBasic().payload as SignedTransactionPayload; + if (block?.body.type === BlockBodyType.Basic && (block?.body as BasicBlockBody).payload?.type === PayloadType.SignedTransaction) { + const payload: SignedTransactionPayload = (block?.body as BasicBlockBody).payload as SignedTransactionPayload; const transactionId = Utils.transactionId(payload); // Unlocks @@ -67,7 +68,6 @@ export class TransactionsHelper { } while (!signatureUnlock); } - if (signatureUnlock) { unlockAddresses.push( Bech32AddressHelper.buildAddress( diff --git a/client/src/models/api/nova/block/IBlockDetailsRequest.ts b/client/src/models/api/nova/block/IBlockDetailsRequest.ts new file mode 100644 index 000000000..065f86323 --- /dev/null +++ b/client/src/models/api/nova/block/IBlockDetailsRequest.ts @@ -0,0 +1,11 @@ +export interface IBlockDetailsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The block id to get the details for. + */ + blockId: string; +} diff --git a/client/src/models/api/nova/block/IBlockDetailsResponse.ts b/client/src/models/api/nova/block/IBlockDetailsResponse.ts new file mode 100644 index 000000000..cbf47c622 --- /dev/null +++ b/client/src/models/api/nova/block/IBlockDetailsResponse.ts @@ -0,0 +1,9 @@ +import { IBlockMetadata } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "../IResponse"; + +export interface IBlockDetailsResponse extends IResponse { + /** + * Block metadata. + */ + metadata?: IBlockMetadata; +} diff --git a/client/src/models/api/nova/block/IBlockMetadata.ts b/client/src/models/api/nova/block/IBlockMetadata.ts new file mode 100644 index 000000000..81a0859cb --- /dev/null +++ b/client/src/models/api/nova/block/IBlockMetadata.ts @@ -0,0 +1,14 @@ +import { IBlockMetadata as BlockMetadata } from "@iota/sdk-wasm-nova/web"; + +export interface IBlockMetadata { + /** + * Metadata. + */ + metadata?: BlockMetadata; + + /** + * The metadata failed. + */ + metadataError?: string; +} + diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index d4d75a0da..1afe057a9 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -5,6 +5,8 @@ import { IOutputDetailsRequest } from "~/models/api/IOutputDetailsRequest"; import { INodeInfoResponse } from "~/models/api/nova/INodeInfoResponse"; import { IOutputDetailsResponse } from "~/models/api/nova/IOutputDetailsResponse"; import { ApiClient } from "../apiClient"; +import { IBlockDetailsRequest } from "~/models/api/nova/block/IBlockDetailsRequest"; +import { IBlockDetailsResponse } from "~/models/api/nova/block/IBlockDetailsResponse"; /** * Class to handle api communications on nova. @@ -33,6 +35,17 @@ export class NovaApiClient extends ApiClient { ); } + /** + * Get the block details. + * @param request The request to send. + * @returns The response from the request. + */ + public async blockDetails(request: IBlockDetailsRequest): Promise { + return this.callApi( + `nova/block/metadata/${request.network}/${request.blockId}`, "get" + ); + } + /** * Get the output details. * @param request The request to send. diff --git a/setup_nova.sh b/setup_nova.sh index 9847b59fc..db0390697 100755 --- a/setup_nova.sh +++ b/setup_nova.sh @@ -1,6 +1,6 @@ #!/bin/bash SDK_DIR="iota-sdk" -TARGET_COMMIT="6628d8ade72a14d0f25eae859590f0bdcea0cf83" +TARGET_COMMIT="8d31e6b6648c1dbd8dcc3777e35bf9865bf2f983" if [ ! -d "$SDK_DIR" ]; then git clone -b 2.0 git@github.com:iotaledger/iota-sdk.git From 85e0497e15530f85e7ff3ceab58919ae090dde98 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Tue, 16 Jan 2024 11:33:08 +0100 Subject: [PATCH 08/15] fix: format --- api/src/routes/nova/block/get.ts | 5 +- api/src/routes/nova/block/metadata/get.ts | 5 +- client/src/app/components/nova/Input.tsx | 23 +--- client/src/app/components/nova/Unlocks.tsx | 102 +++++++-------- .../payload/SignedTransactionPayload.tsx | 11 +- .../nova/block/payload/TaggedDataPayload.tsx | 17 +-- .../block/section/BlockPayloadSection.tsx | 46 +++---- .../section/TransactionMetadataSection.tsx | 29 ++--- client/src/app/routes.tsx | 15 +-- client/src/app/routes/nova/Block.tsx | 123 +++++++----------- client/src/helpers/nova/hooks/useBlock.ts | 32 +++-- .../helpers/nova/hooks/useBlockMetadata.ts | 16 +-- .../helpers/nova/hooks/useInputsAndOutputs.ts | 21 +-- client/src/helpers/nova/transactionsHelper.ts | 91 +++++++------ client/src/models/api/nova/IInput.ts | 1 - client/src/models/api/nova/IOutput.ts | 1 - .../models/api/nova/block/IBlockMetadata.ts | 1 - .../models/api/nova/block/IBlockRequest.ts | 1 - .../models/api/nova/block/IBlockResponse.ts | 1 - 19 files changed, 219 insertions(+), 322 deletions(-) diff --git a/api/src/routes/nova/block/get.ts b/api/src/routes/nova/block/get.ts index 61e71b230..8fb82a0d3 100644 --- a/api/src/routes/nova/block/get.ts +++ b/api/src/routes/nova/block/get.ts @@ -13,10 +13,7 @@ import { ValidationHelper } from "../../../utils/validationHelper"; * @param request The request. * @returns The response. */ -export async function get( - _: IConfiguration, - request: IBlockRequest -): Promise { +export async function get(_: IConfiguration, request: IBlockRequest): Promise { const networkService = ServiceFactory.get("network"); const networks = networkService.networkNames(); ValidationHelper.oneOf(request.network, networks, "network"); diff --git a/api/src/routes/nova/block/metadata/get.ts b/api/src/routes/nova/block/metadata/get.ts index 2850ceb93..487aec349 100644 --- a/api/src/routes/nova/block/metadata/get.ts +++ b/api/src/routes/nova/block/metadata/get.ts @@ -13,10 +13,7 @@ import { ValidationHelper } from "../../../../utils/validationHelper"; * @param request The request. * @returns The response. */ -export async function get( - _: IConfiguration, - request: IBlockRequest -): Promise { +export async function get(_: IConfiguration, request: IBlockRequest): Promise { const networkService = ServiceFactory.get("network"); const networks = networkService.networkNames(); ValidationHelper.oneOf(request.network, networks, "network"); diff --git a/client/src/app/components/nova/Input.tsx b/client/src/app/components/nova/Input.tsx index 399f557e9..0b5a58600 100644 --- a/client/src/app/components/nova/Input.tsx +++ b/client/src/app/components/nova/Input.tsx @@ -33,10 +33,7 @@ const Input: React.FC = ({ input, network }) => { const fallbackInputView = ( -
setIsExpanded(!isExpanded)} - > +
setIsExpanded(!isExpanded)}>
@@ -45,7 +42,7 @@ const Input: React.FC = ({ input, network }) => {
{input.amount && ( { + onClick={(e) => { setIsFormattedBalance(!isFormattedBalance); e.stopPropagation(); }} @@ -72,26 +69,20 @@ const Input: React.FC = ({ input, network }) => {
Transaction Id
- + {input.transactionId}
Transaction Output Index
{input.transactionInputIndex}
-
)} + + )} ); - const outputId = Utils.computeOutputId( - input.transactionId, input.transactionInputIndex - ); + const outputId = Utils.computeOutputId(input.transactionId, input.transactionInputIndex); - return input.output ? - : fallbackInputView; + return input.output ? : fallbackInputView; }; export default Input; diff --git a/client/src/app/components/nova/Unlocks.tsx b/client/src/app/components/nova/Unlocks.tsx index 03899dd6d..38161f2c2 100644 --- a/client/src/app/components/nova/Unlocks.tsx +++ b/client/src/app/components/nova/Unlocks.tsx @@ -1,4 +1,14 @@ -import { AccountUnlock, AnchorUnlock, EmptyUnlock, MultiUnlock, NftUnlock, ReferenceUnlock, SignatureUnlock, Unlock, UnlockType } from "@iota/sdk-wasm-nova/web"; +import { + AccountUnlock, + AnchorUnlock, + EmptyUnlock, + MultiUnlock, + NftUnlock, + ReferenceUnlock, + SignatureUnlock, + Unlock, + UnlockType, +} from "@iota/sdk-wasm-nova/web"; import classNames from "classnames"; import React, { useState } from "react"; import DropdownIcon from "~assets/dropdown-arrow.svg?react"; @@ -39,75 +49,65 @@ const Unlocks: React.FC = ({ unlocks }) => { return (
-
setIsExpanded(!isExpanded)} - className="card--value card-header--wrapper" - > +
setIsExpanded(!isExpanded)} className="card--value card-header--wrapper">
- +
- { - isExpanded && ( -
- {unlocks.map((unlock, idx) => { - if (unlock.type === UnlockType.Signature) { + {isExpanded && ( +
+ {unlocks.map((unlock, idx) => { + if (unlock.type === UnlockType.Signature) { const signatureUnlock = unlock as SignatureUnlock; return (
- {displayUnlocksTypeAndIndex(unlock.type, idx)} -
- Public Key: -
- + {displayUnlocksTypeAndIndex(unlock.type, idx)} +
+ Public Key: +
+ +
-
-
- Signature: -
- +
+ Signature: +
+ +
-
); - } else if (unlock.type === UnlockType.Multi) { - const multiUnlock = unlock as MultiUnlock; + } else if (unlock.type === UnlockType.Multi) { + const multiUnlock = unlock as MultiUnlock; - return ; - } - else if (unlock.type === UnlockType.Empty) { - return ( -
- {displayUnlocksTypeAndIndex(unlock.type, idx)} -
- ); - } - else { - const referencedUnlock = unlock as UnlockTypeMap[typeof unlock.type]; + return ; + } else if (unlock.type === UnlockType.Empty) { + return ( +
+ {displayUnlocksTypeAndIndex(unlock.type, idx)} +
+ ); + } else { + const referencedUnlock = unlock as UnlockTypeMap[typeof unlock.type]; - return ( -
- {displayUnlocksTypeAndIndex(unlock.type, idx)} -
- References unlock at index: - {referencedUnlock.reference} -
+ return ( +
+ {displayUnlocksTypeAndIndex(unlock.type, idx)} +
+ References unlock at index: + {referencedUnlock.reference}
- ); - } - })} -
- ) - } +
+ ); + } + })} +
+ )}
); }; export default Unlocks; - diff --git a/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx b/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx index 5e93415bd..553ada80d 100644 --- a/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx +++ b/client/src/app/components/nova/block/payload/SignedTransactionPayload.tsx @@ -13,11 +13,9 @@ interface SignedTransactionPayloadProps { readonly header?: string; } -const SignedTransactionPayload: React.FC = ( - { payload, inputs, header } -) => { +const SignedTransactionPayload: React.FC = ({ payload, inputs, header }) => { const { networkId, outputs } = payload.transaction; - const transactionId = Utils.transactionId(payload) + const transactionId = Utils.transactionId(payload); return (
@@ -37,7 +35,9 @@ const SignedTransactionPayload: React.FC = ( {inputs.length}
- {inputs.map((input, idx) => )} + {inputs.map((input, idx) => ( + + ))}
@@ -71,4 +71,3 @@ SignedTransactionPayload.defaultProps = { }; export default SignedTransactionPayload; - diff --git a/client/src/app/components/nova/block/payload/TaggedDataPayload.tsx b/client/src/app/components/nova/block/payload/TaggedDataPayload.tsx index 8c893dc36..defb6b430 100644 --- a/client/src/app/components/nova/block/payload/TaggedDataPayload.tsx +++ b/client/src/app/components/nova/block/payload/TaggedDataPayload.tsx @@ -1,4 +1,4 @@ -import { TaggedDataPayload as ITaggedDataPayload} from "@iota/sdk-wasm-nova/web"; +import { TaggedDataPayload as ITaggedDataPayload } from "@iota/sdk-wasm-nova/web"; import React from "react"; import DataToggle from "~/app/components/DataToggle"; @@ -6,9 +6,7 @@ interface TaggedDataPayloadProps { readonly payload: ITaggedDataPayload; } -const TaggedDataPayload: React.FC = ( - { payload } -) => { +const TaggedDataPayload: React.FC = ({ payload }) => { const { tag, data } = payload; return ( @@ -19,10 +17,7 @@ const TaggedDataPayload: React.FC = (
Tag
- + )} {data && ( @@ -30,10 +25,7 @@ const TaggedDataPayload: React.FC = (
Data
- + )}
@@ -46,4 +38,3 @@ TaggedDataPayload.defaultProps = { }; export default TaggedDataPayload; - diff --git a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx index 374de777d..4bfb2791b 100644 --- a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx +++ b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx @@ -1,7 +1,9 @@ import { - Block, PayloadType, SignedTransactionPayload as ISignedTransactionPayload, + Block, + PayloadType, + SignedTransactionPayload as ISignedTransactionPayload, TaggedDataPayload as ITaggedDataPayload, - BasicBlockBody + BasicBlockBody, } from "@iota/sdk-wasm-nova/web"; import React from "react"; import { IInput } from "~models/api/nova/IInput"; @@ -16,52 +18,35 @@ interface BlockPayloadSectionProps { readonly transferTotal?: number; } -const BlockPayloadSection: React.FC = ( - { block, inputs, outputs, transferTotal } -) => { - const payload = (block.body as BasicBlockBody).payload - if ( - payload?.type === PayloadType.SignedTransaction && - inputs && outputs && transferTotal !== undefined - ) { - const transactionPayload = payload as ISignedTransactionPayload; +const BlockPayloadSection: React.FC = ({ block, inputs, outputs, transferTotal }) => { + const payload = (block.body as BasicBlockBody).payload; + if (payload?.type === PayloadType.SignedTransaction && inputs && outputs && transferTotal !== undefined) { + const transactionPayload = payload as ISignedTransactionPayload; const transaction = transactionPayload.transaction; return (
- +
- { - transaction.payload?.type === PayloadType.TaggedData && + {transaction.payload?.type === PayloadType.TaggedData && (
- +
- } + )}
); - } else if ( - payload?.type === PayloadType.CandidacyAnnouncement - ) { + } else if (payload?.type === PayloadType.CandidacyAnnouncement) { return (
{/* todo */} CandidacyAnnouncement
); - } else if ( - payload?.type === PayloadType.TaggedData - ) { + } else if (payload?.type === PayloadType.TaggedData) { return (
- +
); } @@ -76,4 +61,3 @@ BlockPayloadSection.defaultProps = { }; export default BlockPayloadSection; - diff --git a/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx b/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx index 8ebfd18bd..7d3098f24 100644 --- a/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx +++ b/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx @@ -10,36 +10,26 @@ interface TransactionMetadataSectionProps { readonly isLinksDisabled: boolean; } -const TransactionMetadataSection: React.FC = ( - { network, metadata, metadataError, isLinksDisabled } -) => ( +const TransactionMetadataSection: React.FC = ({ network, metadata, metadataError, isLinksDisabled }) => (
- {!metadata && !metadataError && ()} - {metadataError && ( -

Failed to retrieve metadata. {metadataError}

- )} + {!metadata && !metadataError && } + {metadataError &&

Failed to retrieve metadata. {metadataError}

} {metadata && !metadataError && (
-
- Transaction Id -
+
Transaction Id
-
-
- Transaction Status -
-
- {metadata.transactionState} -
+
Transaction Status
+
{metadata.transactionState}
{metadata.transactionFailureReason && (
@@ -55,8 +45,7 @@ const TransactionMetadataSection: React.FC = ( TransactionMetadataSection.defaultProps = { metadata: undefined, - metadataError: undefined + metadataError: undefined, }; export default TransactionMetadataSection; - diff --git a/client/src/app/routes.tsx b/client/src/app/routes.tsx index edcb544b3..178b4c892 100644 --- a/client/src/app/routes.tsx +++ b/client/src/app/routes.tsx @@ -167,18 +167,9 @@ const buildAppRoutes = (protocolVersion: string, withNetworkContext: (wrappedCom ]; const novaRoutes = [ - , - , - , + , + , + , ]; return ( diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index 96cde4611..f22b92ad5 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -1,4 +1,3 @@ - import React, { useState } from "react"; import { RouteComponentProps } from "react-router-dom"; import mainHeaderMessage from "~assets/modals/stardust/block/main-header.json"; @@ -33,9 +32,12 @@ export interface BlockProps { blockId: string; } -const Block: React.FC> = ( - { history, match: { params: { network, blockId } } } -) => { +const Block: React.FC> = ({ + history, + match: { + params: { network, blockId }, + }, +}) => { const { networkInfo } = useNetworkInfoNova(); const [isFormattedBalance, setIsFormattedBalance] = useState(true); const [block, isLoading, blockError] = useBlock(network, blockId); @@ -45,18 +47,18 @@ const Block: React.FC> = ( function isBasicBlockBody(body: BasicBlockBody | ValidationBlockBody): body is BasicBlockBody { return body.type === BlockBodyType.Basic; } - let blockBody: BasicBlockBody | ValidationBlockBody | undefined + let blockBody: BasicBlockBody | ValidationBlockBody | undefined; let pageTitle = "Block"; switch (block?.body?.type) { case BlockBodyType.Basic: { pageTitle = `Basic ${pageTitle}`; - blockBody = block?.body as BasicBlockBody + blockBody = block?.body as BasicBlockBody; break; } case BlockBodyType.Validation: { pageTitle = `Validation ${pageTitle}`; - blockBody = block?.body as ValidationBlockBody + blockBody = block?.body as ValidationBlockBody; break; } default: { @@ -74,10 +76,10 @@ const Block: React.FC> = ( inputs={inputs ?? undefined} outputs={outputs ?? undefined} transferTotal={transferTotal ?? undefined} - /> + />, ); } - + if (blockMetadata.metadata?.transactionMetadata) { tabbedSections.push( > = ( network={network} metadata={blockMetadata.metadata?.transactionMetadata} isLinksDisabled={false} - /> + />, ); } - - + const blockContent = block ? (
@@ -98,51 +99,37 @@ const Block: React.FC> = (
-
- Block ID -
+
Block ID
-
- Issuing Time -
+
Issuing Time
{/* Convert nanoseconds to milliseconds */} {DateHelper.formatShort(Number(block.header.issuingTime) / 1000000)}
-
- Slot commitment -
+
Slot commitment
-
- Issuer -
+
Issuer
- +
{blockBody?.strongParents && (
-
- Strong Parents -
+
Strong Parents
{blockBody.strongParents.map((parent, idx) => ( -
+
> = ( )} {blockBody?.weakParents && (
-
- Weak Parents -
+
Weak Parents
{blockBody.weakParents.map((child, idx) => ( -
+
> = (
{blockBody && isBasicBlockBody(blockBody) && (
- {blockBody.payload?.type === PayloadType.SignedTransaction && - transferTotal !== null && ( + {blockBody.payload?.type === PayloadType.SignedTransaction && transferTotal !== null && (
-
- Amount transacted -
+
Amount transacted
- setIsFormattedBalance(!isFormattedBalance)} - className="pointer margin-r-5" - > - {formatAmount( - transferTotal, - networkInfo.tokenInfo, - !isFormattedBalance - )} + setIsFormattedBalance(!isFormattedBalance)} className="pointer margin-r-5"> + {formatAmount(transferTotal, networkInfo.tokenInfo, !isFormattedBalance)}
@@ -197,23 +168,25 @@ const Block: React.FC> = ( {tabbedSections} @@ -231,16 +204,11 @@ const Block: React.FC> = (
-

- {pageTitle} -

+

{pageTitle}

- +
@@ -268,4 +236,3 @@ const Block: React.FC> = ( }; export default Block; - diff --git a/client/src/helpers/nova/hooks/useBlock.ts b/client/src/helpers/nova/hooks/useBlock.ts index 177b16477..d94e63db4 100644 --- a/client/src/helpers/nova/hooks/useBlock.ts +++ b/client/src/helpers/nova/hooks/useBlock.ts @@ -12,12 +12,7 @@ import { HexHelper } from "~/helpers/stardust/hexHelper"; * @param blockId The block id * @returns The block, loading bool and an error message. */ -export function useBlock(network: string, blockId: string | null): - [ - Block | null, - boolean, - string? - ] { +export function useBlock(network: string, blockId: string | null): [Block | null, boolean, string?] { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); const [block, setBlock] = useState(null); @@ -30,17 +25,20 @@ export function useBlock(network: string, blockId: string | null): if (blockId) { // eslint-disable-next-line no-void void (async () => { - apiClient.block({ - network, - blockId: HexHelper.addPrefix(blockId) - }).then(response => { - if (isMounted) { - setBlock(response.block ?? null); - setError(response.error); - } - }).finally(() => { - setIsLoading(false); - }); + apiClient + .block({ + network, + blockId: HexHelper.addPrefix(blockId), + }) + .then((response) => { + if (isMounted) { + setBlock(response.block ?? null); + setError(response.error); + } + }) + .finally(() => { + setIsLoading(false); + }); })(); } else { setIsLoading(false); diff --git a/client/src/helpers/nova/hooks/useBlockMetadata.ts b/client/src/helpers/nova/hooks/useBlockMetadata.ts index 7fe23bdcc..558d15cff 100644 --- a/client/src/helpers/nova/hooks/useBlockMetadata.ts +++ b/client/src/helpers/nova/hooks/useBlockMetadata.ts @@ -12,30 +12,26 @@ import { IBlockMetadata } from "~/models/api/nova/block/IBlockMetadata"; * @param blockId The block id * @returns The block metadata and loading bool. */ -export function useBlockMetadata(network: string, blockId: string | null): - [ - IBlockMetadata, - boolean - ] { +export function useBlockMetadata(network: string, blockId: string | null): [IBlockMetadata, boolean] { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); - const [blockMetadata, setBlockMetadata] = useState({ metadata: { blockId: blockId ?? '', blockState: "pending"} }); + const [blockMetadata, setBlockMetadata] = useState({ metadata: { blockId: blockId ?? "", blockState: "pending" } }); const [isLoading, setIsLoading] = useState(true); useEffect(() => { let timerId: NodeJS.Timeout | undefined; setIsLoading(true); if (blockId) { - setBlockMetadata({ metadata: { blockId, blockState: "pending"} }); + setBlockMetadata({ metadata: { blockId, blockState: "pending" } }); const fetchMetadata = async () => { try { const details = await apiClient.blockDetails({ network, - blockId: HexHelper.addPrefix(blockId) + blockId: HexHelper.addPrefix(blockId), }); if (isMounted) { - setBlockMetadata({metadata: details?.metadata}); + setBlockMetadata({ metadata: details?.metadata }); if (!details?.metadata) { timerId = setTimeout(async () => { @@ -45,7 +41,7 @@ export function useBlockMetadata(network: string, blockId: string | null): } } catch (error) { if (error instanceof Error && isMounted) { - setBlockMetadata({ metadataError: error.message}); + setBlockMetadata({ metadataError: error.message }); } } finally { setIsLoading(false); diff --git a/client/src/helpers/nova/hooks/useInputsAndOutputs.ts b/client/src/helpers/nova/hooks/useInputsAndOutputs.ts index c655c6072..754f38fb1 100644 --- a/client/src/helpers/nova/hooks/useInputsAndOutputs.ts +++ b/client/src/helpers/nova/hooks/useInputsAndOutputs.ts @@ -15,13 +15,7 @@ import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; * @param block The block * @returns The inputs, unlocks, outputs, transfer total an a loading bool. */ -export function useInputsAndOutputs(network: string, block: Block | null): - [ - IInput[] | null, - IOutput[] | null, - number | null, - boolean - ] { +export function useInputsAndOutputs(network: string, block: Block | null): [IInput[] | null, IOutput[] | null, number | null, boolean] { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${STARDUST}`)); const { networkInfo } = useNetworkInfoNova(); @@ -36,13 +30,12 @@ export function useInputsAndOutputs(network: string, block: Block | null): if (block && (block?.body as BasicBlockBody).payload?.type === PayloadType.SignedTransaction) { // eslint-disable-next-line no-void void (async () => { - const { inputs, outputs, transferTotal } = - await TransactionsHelper.getInputsAndOutputs( - block, - network, - networkInfo.bech32Hrp, - apiClient - ); + const { inputs, outputs, transferTotal } = await TransactionsHelper.getInputsAndOutputs( + block, + network, + networkInfo.bech32Hrp, + apiClient, + ); if (isMounted) { setInputs(inputs); setOutputs(outputs); diff --git a/client/src/helpers/nova/transactionsHelper.ts b/client/src/helpers/nova/transactionsHelper.ts index 7c98fa656..486d5bdf7 100644 --- a/client/src/helpers/nova/transactionsHelper.ts +++ b/client/src/helpers/nova/transactionsHelper.ts @@ -1,13 +1,23 @@ import { AddressUnlockCondition, BasicBlockBody, - Block, BlockBodyType, CommonOutput, DelegationOutput, GovernorAddressUnlockCondition, + Block, + BlockBodyType, + CommonOutput, + DelegationOutput, + GovernorAddressUnlockCondition, ImmutableAccountAddressUnlockCondition, - InputType,OutputType, PayloadType, + InputType, + OutputType, + PayloadType, SignatureUnlock, SignedTransactionPayload, StateControllerAddressUnlockCondition, - UnlockCondition, UnlockConditionType, UnlockType, Utils, UTXOInput + UnlockCondition, + UnlockConditionType, + UnlockType, + Utils, + UTXOInput, } from "@iota/sdk-wasm-nova/web"; import { IBech32AddressDetails } from "~/models/api/IBech32AddressDetails"; import { IInput } from "~/models/api/nova/IInput"; @@ -24,8 +34,11 @@ interface TransactionInputsAndOutputsResponse { } export class TransactionsHelper { - public static async getInputsAndOutputs(block: Block | undefined, network: string, - _bechHrp: string, apiClient: NovaApiClient + public static async getInputsAndOutputs( + block: Block | undefined, + network: string, + _bechHrp: string, + apiClient: NovaApiClient, ): Promise { const GENESIS_HASH = "0".repeat(64); const inputs: IInput[] = []; @@ -49,31 +62,29 @@ export class TransactionsHelper { if (unlock.type === UnlockType.Signature) { signatureUnlock = unlock as SignatureUnlock; - } - else { + } else { let refUnlockIdx = i; // unlock references can be transitive, // so we need to follow the path until we find the signature do { - const referencedUnlock = unlocks[refUnlockIdx] + const referencedUnlock = unlocks[refUnlockIdx]; if (referencedUnlock.type === UnlockType.Signature) { - signatureUnlock = referencedUnlock as SignatureUnlock + signatureUnlock = referencedUnlock as SignatureUnlock; } else if (referencedUnlock.type === UnlockType.Multi || referencedUnlock.type === UnlockType.Empty) { break; } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any refUnlockIdx = (referencedUnlock as any).reference; } - } while (!signatureUnlock); } if (signatureUnlock) { unlockAddresses.push( Bech32AddressHelper.buildAddress( _bechHrp, - Utils.hexPublicKeyToBech32Address(signatureUnlock.signature.publicKey, _bechHrp) - ) + Utils.hexPublicKeyToBech32Address(signatureUnlock.signature.publicKey, _bechHrp), + ), ); } } @@ -92,10 +103,7 @@ export class TransactionsHelper { const utxoInput = input as UTXOInput; isGenesis = utxoInput.transactionId === GENESIS_HASH; - const outputId = Utils.computeOutputId( - utxoInput.transactionId, - utxoInput.transactionOutputIndex - ); + const outputId = Utils.computeOutputId(utxoInput.transactionId, utxoInput.transactionOutputIndex); const response = await apiClient.outputDetails({ network, outputId }); const details = response.output; @@ -103,7 +111,7 @@ export class TransactionsHelper { if (!response.error && details?.output && details?.metadata) { outputDetails = { output: details.output, - metadata: details.metadata + metadata: details.metadata, }; amount = Number(details.output.amount); } @@ -116,7 +124,7 @@ export class TransactionsHelper { isGenesis, outputId, output: outputDetails, - address + address, }); } } @@ -131,16 +139,18 @@ export class TransactionsHelper { outputs.push({ id: outputId, output, - amount: Number(transaction.outputs[i].amount) + amount: Number(transaction.outputs[i].amount), }); } else { const output = transaction.outputs[i] as CommonOutput; const address: IBech32AddressDetails = TransactionsHelper.bechAddressFromAddressUnlockCondition( - output.unlockConditions, _bechHrp, output.type + output.unlockConditions, + _bechHrp, + output.type, ); - const isRemainder = inputs.some(input => input.address.bech32 === address.bech32); + const isRemainder = inputs.some((input) => input.address.bech32 === address.bech32); if (isRemainder) { remainderOutputs.push({ @@ -148,7 +158,7 @@ export class TransactionsHelper { address, amount: Number(transaction.outputs[i].amount), isRemainder, - output + output, }); } else { outputs.push({ @@ -156,7 +166,7 @@ export class TransactionsHelper { address, amount: Number(transaction.outputs[i].amount), isRemainder, - output + output, }); } @@ -174,7 +184,6 @@ export class TransactionsHelper { return { inputs, outputs: sortedOutputs, unlockAddresses, transferTotal }; } - /** * Sort inputs and outputs in assending order by index. * @param items Inputs or Outputs. @@ -194,36 +203,36 @@ export class TransactionsHelper { private static bechAddressFromAddressUnlockCondition( unlockConditions: UnlockCondition[], _bechHrp: string, - outputType: number + outputType: number, ): IBech32AddressDetails { let address: IBech32AddressDetails = { bech32: "" }; let unlockCondition; if (outputType === OutputType.Basic || outputType === OutputType.Nft) { - unlockCondition = unlockConditions?.filter( - ot => ot.type === UnlockConditionType.Address - ).map(ot => ot as AddressUnlockCondition)[0]; + unlockCondition = unlockConditions + ?.filter((ot) => ot.type === UnlockConditionType.Address) + .map((ot) => ot as AddressUnlockCondition)[0]; } else if (outputType === OutputType.Account) { - if (unlockConditions.some(ot => ot.type === UnlockConditionType.StateControllerAddress)) { - unlockCondition = unlockConditions?.filter( - ot => ot.type === UnlockConditionType.StateControllerAddress - ).map(ot => ot as StateControllerAddressUnlockCondition)[0]; + if (unlockConditions.some((ot) => ot.type === UnlockConditionType.StateControllerAddress)) { + unlockCondition = unlockConditions + ?.filter((ot) => ot.type === UnlockConditionType.StateControllerAddress) + .map((ot) => ot as StateControllerAddressUnlockCondition)[0]; } - if (unlockConditions.some(ot => ot.type === UnlockConditionType.GovernorAddress)) { - unlockCondition = unlockConditions?.filter( - ot => ot.type === UnlockConditionType.GovernorAddress - ).map(ot => ot as GovernorAddressUnlockCondition)[0]; + if (unlockConditions.some((ot) => ot.type === UnlockConditionType.GovernorAddress)) { + unlockCondition = unlockConditions + ?.filter((ot) => ot.type === UnlockConditionType.GovernorAddress) + .map((ot) => ot as GovernorAddressUnlockCondition)[0]; } } else if (outputType === OutputType.Foundry) { - unlockCondition = unlockConditions?.filter( - ot => ot.type === UnlockConditionType.ImmutableAccountAddress - ).map(ot => ot as ImmutableAccountAddressUnlockCondition)[0]; + unlockCondition = unlockConditions + ?.filter((ot) => ot.type === UnlockConditionType.ImmutableAccountAddress) + .map((ot) => ot as ImmutableAccountAddressUnlockCondition)[0]; } if (unlockCondition?.address) { - address = { bech32: unlockCondition?.address.toString()}; + address = { bech32: unlockCondition?.address.toString() }; } return address; } -} \ No newline at end of file +} diff --git a/client/src/models/api/nova/IInput.ts b/client/src/models/api/nova/IInput.ts index 42e86006e..a9cd50399 100644 --- a/client/src/models/api/nova/IInput.ts +++ b/client/src/models/api/nova/IInput.ts @@ -31,4 +31,3 @@ export interface IInput { */ isGenesis: boolean; } - diff --git a/client/src/models/api/nova/IOutput.ts b/client/src/models/api/nova/IOutput.ts index 47765b354..59c34c61c 100644 --- a/client/src/models/api/nova/IOutput.ts +++ b/client/src/models/api/nova/IOutput.ts @@ -23,4 +23,3 @@ export interface IOutput { */ isRemainder?: boolean; } - diff --git a/client/src/models/api/nova/block/IBlockMetadata.ts b/client/src/models/api/nova/block/IBlockMetadata.ts index 81a0859cb..54c035144 100644 --- a/client/src/models/api/nova/block/IBlockMetadata.ts +++ b/client/src/models/api/nova/block/IBlockMetadata.ts @@ -11,4 +11,3 @@ export interface IBlockMetadata { */ metadataError?: string; } - diff --git a/client/src/models/api/nova/block/IBlockRequest.ts b/client/src/models/api/nova/block/IBlockRequest.ts index e22b99aab..781d93d64 100644 --- a/client/src/models/api/nova/block/IBlockRequest.ts +++ b/client/src/models/api/nova/block/IBlockRequest.ts @@ -9,4 +9,3 @@ export interface IBlockRequest { */ blockId: string; } - diff --git a/client/src/models/api/nova/block/IBlockResponse.ts b/client/src/models/api/nova/block/IBlockResponse.ts index a688d366d..f2239c888 100644 --- a/client/src/models/api/nova/block/IBlockResponse.ts +++ b/client/src/models/api/nova/block/IBlockResponse.ts @@ -7,4 +7,3 @@ export interface IBlockResponse extends IResponse { */ block: Block; } - From 5d1bd857af1bee58e561edb9552f3dd28c18fe56 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Thu, 18 Jan 2024 08:38:56 +0100 Subject: [PATCH 09/15] fix: add max burned mana --- client/src/app/routes/nova/Block.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index f22b92ad5..24b70e0d6 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -154,6 +154,12 @@ const Block: React.FC> = ({
{blockBody && isBasicBlockBody(blockBody) && (
+
+
Max burned mana
+
+ {Number(blockBody.maxBurnedMana)} +
+
{blockBody.payload?.type === PayloadType.SignedTransaction && transferTotal !== null && (
Amount transacted
From 378001a09076cd9456a053da8cd12482cbcfffec Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Thu, 18 Jan 2024 14:34:14 +0100 Subject: [PATCH 10/15] fix: improve block with transaction payload --- .../app/components/nova/ContextInputView.tsx | 50 +++++++++ .../block/section/BlockPayloadSection.tsx | 19 ++++ .../section/TransactionMetadataSection.tsx | 102 ++++++++++++------ client/src/app/routes/nova/Block.tsx | 89 +++++++++------ 4 files changed, 190 insertions(+), 70 deletions(-) create mode 100644 client/src/app/components/nova/ContextInputView.tsx diff --git a/client/src/app/components/nova/ContextInputView.tsx b/client/src/app/components/nova/ContextInputView.tsx new file mode 100644 index 000000000..279ff9777 --- /dev/null +++ b/client/src/app/components/nova/ContextInputView.tsx @@ -0,0 +1,50 @@ +import { + BlockIssuanceCreditContextInput, + CommitmentContextInput, + ContextInput, + ContextInputType, + RewardContextInput, +} from "@iota/sdk-wasm-nova/web"; +import React from "react"; +import TruncatedId from "../stardust/TruncatedId"; +import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; + +interface IContextInputViewProps { + readonly contextInput: ContextInput; +} + +const ContextInputView: React.FC = ({ contextInput }) => { + const { networkInfo } = useNetworkInfoNova(); + if (contextInput.type === ContextInputType.COMMITMENT) { + const input = contextInput as CommitmentContextInput; + + return ( +
+
Commitment id
+
{input.commitmentId}
+
+ ); + } else if (contextInput.type === ContextInputType.BLOCK_ISSUANCE_CREDIT) { + const input = contextInput as BlockIssuanceCreditContextInput; + + return ( +
+
Account
+
+ +
+
+ ); + } else if (contextInput.type === ContextInputType.REWARD) { + const input = contextInput as RewardContextInput; + + return ( +
+
Reward Input Index
+
{input.index}
+
+ ); + } +}; + +export default ContextInputView; diff --git a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx index 4bfb2791b..08680ba7b 100644 --- a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx +++ b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx @@ -4,12 +4,15 @@ import { SignedTransactionPayload as ISignedTransactionPayload, TaggedDataPayload as ITaggedDataPayload, BasicBlockBody, + Utils, } from "@iota/sdk-wasm-nova/web"; import React from "react"; import { IInput } from "~models/api/nova/IInput"; import { IOutput } from "~models/api/nova/IOutput"; import TaggedDataPayload from "../payload/TaggedDataPayload"; import SignedTransactionPayload from "../payload/SignedTransactionPayload"; +import TruncatedId from "~/app/components/stardust/TruncatedId"; +import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; interface BlockPayloadSectionProps { readonly block: Block; @@ -19,10 +22,16 @@ interface BlockPayloadSectionProps { } const BlockPayloadSection: React.FC = ({ block, inputs, outputs, transferTotal }) => { + const { networkInfo } = useNetworkInfoNova(); const payload = (block.body as BasicBlockBody).payload; + if (payload?.type === PayloadType.SignedTransaction && inputs && outputs && transferTotal !== undefined) { const transactionPayload = payload as ISignedTransactionPayload; const transaction = transactionPayload.transaction; + const nestedTransactionId = + transaction.payload?.type === PayloadType.SignedTransaction + ? Utils.transactionId(transaction.payload as ISignedTransactionPayload) + : undefined; return ( @@ -34,6 +43,16 @@ const BlockPayloadSection: React.FC = ({ block, inputs
)} + {nestedTransactionId && ( +
+
+
Transaction
+
+ +
+
+
+ )} ); } else if (payload?.type === PayloadType.CandidacyAnnouncement) { diff --git a/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx b/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx index 7d3098f24..dc1ce4219 100644 --- a/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx +++ b/client/src/app/components/nova/block/section/TransactionMetadataSection.tsx @@ -1,50 +1,82 @@ -import { TransactionMetadata } from "@iota/sdk-wasm-nova/web"; +import { Transaction, TransactionMetadata } from "@iota/sdk-wasm-nova/web"; import React from "react"; import Spinner from "../../../Spinner"; import TruncatedId from "~/app/components/stardust/TruncatedId"; +import ContextInputView from "../../ContextInputView"; +import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; interface TransactionMetadataSectionProps { readonly network: string; - readonly metadata?: TransactionMetadata; + readonly transaction?: Transaction; + readonly transactionMetadata?: TransactionMetadata; readonly metadataError?: string; - readonly isLinksDisabled: boolean; } -const TransactionMetadataSection: React.FC = ({ network, metadata, metadataError, isLinksDisabled }) => ( -
-
- {!metadata && !metadataError && } - {metadataError &&

Failed to retrieve metadata. {metadataError}

} - {metadata && !metadataError && ( - -
-
Transaction Id
-
- -
-
-
-
Transaction Status
-
{metadata.transactionState}
-
- {metadata.transactionFailureReason && ( -
-
Failure Reason
-
{metadata.transactionFailureReason}
-
- )} -
- )} +const TransactionMetadataSection: React.FC = ({ + network, + transaction, + transactionMetadata, + metadataError, +}) => { + const { networkInfo } = useNetworkInfoNova(); + + return ( +
+
+ {!transactionMetadata && !metadataError && } + {metadataError ? ( +

Failed to retrieve metadata. {metadataError}

+ ) : ( + + {transactionMetadata && ( + <> +
+
Transaction Status
+
{transactionMetadata.transactionState}
+
+ {transactionMetadata.transactionFailureReason && ( +
+
Failure Reason
+
{transactionMetadata.transactionFailureReason}
+
+ )} + + )} + {transaction && ( + <> +
+
Creation slot
+
{transaction.creationSlot}
+
+ {transaction?.contextInputs?.map((contxtInput, idx) => ( + + ))} + {transaction?.allotments && ( +
+
Mana Allotment Accounts
+ {transaction?.allotments?.map((allotment, idx) => ( +
+ +
+ ))} +
+ )} + + )} +
+ )} +
-
-); + ); +}; TransactionMetadataSection.defaultProps = { - metadata: undefined, + transactionMetadata: undefined, + transaction: undefined, metadataError: undefined, }; diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index 24b70e0d6..a38b5b112 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -1,10 +1,18 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { RouteComponentProps } from "react-router-dom"; import mainHeaderMessage from "~assets/modals/stardust/block/main-header.json"; import metadataInfo from "~assets/modals/stardust/block/metadata.json"; import { useBlock } from "~helpers/nova/hooks/useBlock"; import NotFound from "../../components/NotFound"; -import { BasicBlockBody, BlockBodyType, PayloadType, ValidationBlockBody } from "@iota/sdk-wasm-nova/web"; +import { + BasicBlockBody, + BlockBody, + BlockBodyType, + PayloadType, + SignedTransactionPayload, + Utils, + ValidationBlockBody, +} from "@iota/sdk-wasm-nova/web"; import Modal from "~/app/components/Modal"; import Spinner from "~/app/components/Spinner"; import TruncatedId from "~/app/components/stardust/TruncatedId"; @@ -43,28 +51,35 @@ const Block: React.FC> = ({ const [block, isLoading, blockError] = useBlock(network, blockId); const [blockMetadata] = useBlockMetadata(network, blockId); const [inputs, outputs, transferTotal] = useInputsAndOutputs(network, block); + const [blockBody, setBlockBody] = useState(); + const [transactionId, setTransactionId] = useState(); + const [pageTitle, setPageTitle] = useState("Block"); - function isBasicBlockBody(body: BasicBlockBody | ValidationBlockBody): body is BasicBlockBody { + function isBasicBlockBody(body: BlockBody): body is BasicBlockBody { return body.type === BlockBodyType.Basic; } - let blockBody: BasicBlockBody | ValidationBlockBody | undefined; - let pageTitle = "Block"; - switch (block?.body?.type) { - case BlockBodyType.Basic: { - pageTitle = `Basic ${pageTitle}`; - blockBody = block?.body as BasicBlockBody; - break; + useEffect(() => { + switch (block?.body?.type) { + case BlockBodyType.Basic: { + setPageTitle(`Basic ${pageTitle}`); + const body = block?.body as BasicBlockBody; + setBlockBody(body); + const tsxId = Utils.transactionId(body.payload as SignedTransactionPayload); + setTransactionId(tsxId); + break; + } + case BlockBodyType.Validation: { + setPageTitle(`Validation ${pageTitle}`); + const body = block?.body as BasicBlockBody; + setBlockBody(body); + break; + } + default: { + break; + } } - case BlockBodyType.Validation: { - pageTitle = `Validation ${pageTitle}`; - blockBody = block?.body as ValidationBlockBody; - break; - } - default: { - break; - } - } + }, [block]); const tabbedSections = []; let idx = 0; @@ -85,8 +100,8 @@ const Block: React.FC> = ({ , ); } @@ -104,6 +119,14 @@ const Block: React.FC> = ({
+ {transactionId && ( +
+
Transaction Id
+
+ +
+
+ )}
Issuing Time
@@ -117,10 +140,14 @@ const Block: React.FC> = ({
+
+
Latest finalized slot
+
{block.header.latestFinalizedSlot}
+
Issuer
-
- +
+
@@ -130,10 +157,7 @@ const Block: React.FC> = ({
Strong Parents
{blockBody.strongParents.map((parent, idx) => (
- +
))}
@@ -143,10 +167,7 @@ const Block: React.FC> = ({
Weak Parents
{blockBody.weakParents.map((child, idx) => (
- +
))}
@@ -156,9 +177,7 @@ const Block: React.FC> = ({
Max burned mana
-
- {Number(blockBody.maxBurnedMana)} -
+
{Number(blockBody.maxBurnedMana)}
{blockBody.payload?.type === PayloadType.SignedTransaction && transferTotal !== null && (
@@ -175,7 +194,7 @@ const Block: React.FC> = ({ key={blockId} tabsEnum={ blockBody.payload?.type === PayloadType.SignedTransaction - ? { Payload: "Transaction Payload", Metadata: "Transaction metadata" } + ? { Payload: "Transaction Payload", Metadata: "Metadata" } : { Payload: "Tagged Data Payload" } } tabOptions={ From f2ec03518d48d4320cfb8263359963675b9398c3 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Thu, 18 Jan 2024 15:00:33 +0100 Subject: [PATCH 11/15] fix: improve validation block --- client/src/app/routes/nova/Block.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index a38b5b112..dca42707f 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -150,7 +150,6 @@ const Block: React.FC> = ({
-
{blockBody?.strongParents && (
@@ -165,14 +164,30 @@ const Block: React.FC> = ({ {blockBody?.weakParents && (
Weak Parents
- {blockBody.weakParents.map((child, idx) => ( + {blockBody.weakParents.map((weak, idx) => ( +
+ +
+ ))} +
+ )} + {blockBody?.shallowLikeParents && ( +
+
Shallow Parents
+ {blockBody.shallowLikeParents.map((shallow, idx) => (
- +
))}
)}
+ {blockBody && !isBasicBlockBody(blockBody) && ( +
+
Highest supported protocol version
+
{blockBody.highestSupportedVersion}
+
+ )} {blockBody && isBasicBlockBody(blockBody) && (
From b8b388bbf41d4562e56320d05ea8f2638404be90 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Thu, 18 Jan 2024 15:30:16 +0100 Subject: [PATCH 12/15] fix: improve candidacy announcement and block page title --- .../block/section/BlockPayloadSection.tsx | 7 ----- client/src/app/routes/nova/Block.tsx | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx index 08680ba7b..443766871 100644 --- a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx +++ b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx @@ -55,13 +55,6 @@ const BlockPayloadSection: React.FC = ({ block, inputs )} ); - } else if (payload?.type === PayloadType.CandidacyAnnouncement) { - return ( -
- {/* todo */} - CandidacyAnnouncement -
- ); } else if (payload?.type === PayloadType.TaggedData) { return (
diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index dca42707f..bb66a4b55 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -59,14 +59,34 @@ const Block: React.FC> = ({ return body.type === BlockBodyType.Basic; } + function updatePageTitle(type: PayloadType | undefined): void { + let title = null; + switch (type) { + case PayloadType.TaggedData: + title = "Data"; + break; + case PayloadType.SignedTransaction: + title = "Transaction"; + break; + case PayloadType.CandidacyAnnouncement: + title = "Candidacy Announcement"; + break; + } + + if (title) { + setPageTitle(`${title} ${pageTitle}`); + } + } useEffect(() => { switch (block?.body?.type) { case BlockBodyType.Basic: { - setPageTitle(`Basic ${pageTitle}`); const body = block?.body as BasicBlockBody; setBlockBody(body); - const tsxId = Utils.transactionId(body.payload as SignedTransactionPayload); - setTransactionId(tsxId); + updatePageTitle(body.payload?.type); + if (body.payload?.type === PayloadType.SignedTransaction) { + const tsxId = Utils.transactionId(body.payload as SignedTransactionPayload); + setTransactionId(tsxId); + } break; } case BlockBodyType.Validation: { From ea00542435c548a7d5323f7c67b19d7badb5ff29 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Thu, 18 Jan 2024 15:44:43 +0100 Subject: [PATCH 13/15] fix: lint --- client/src/app/components/nova/ContextInputView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/app/components/nova/ContextInputView.tsx b/client/src/app/components/nova/ContextInputView.tsx index 279ff9777..91498eefa 100644 --- a/client/src/app/components/nova/ContextInputView.tsx +++ b/client/src/app/components/nova/ContextInputView.tsx @@ -45,6 +45,8 @@ const ContextInputView: React.FC = ({ contextInput }) =>
); } + + return null; }; export default ContextInputView; From 41620da9d263f60d63fd542f3c0fcfaa0b8d1753 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Wed, 24 Jan 2024 09:40:18 +0100 Subject: [PATCH 14/15] fix: remove nested transaction from block payload section --- .../nova/block/section/BlockPayloadSection.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx index 443766871..6f0dffbfb 100644 --- a/client/src/app/components/nova/block/section/BlockPayloadSection.tsx +++ b/client/src/app/components/nova/block/section/BlockPayloadSection.tsx @@ -4,15 +4,12 @@ import { SignedTransactionPayload as ISignedTransactionPayload, TaggedDataPayload as ITaggedDataPayload, BasicBlockBody, - Utils, } from "@iota/sdk-wasm-nova/web"; import React from "react"; import { IInput } from "~models/api/nova/IInput"; import { IOutput } from "~models/api/nova/IOutput"; import TaggedDataPayload from "../payload/TaggedDataPayload"; import SignedTransactionPayload from "../payload/SignedTransactionPayload"; -import TruncatedId from "~/app/components/stardust/TruncatedId"; -import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; interface BlockPayloadSectionProps { readonly block: Block; @@ -22,16 +19,11 @@ interface BlockPayloadSectionProps { } const BlockPayloadSection: React.FC = ({ block, inputs, outputs, transferTotal }) => { - const { networkInfo } = useNetworkInfoNova(); const payload = (block.body as BasicBlockBody).payload; if (payload?.type === PayloadType.SignedTransaction && inputs && outputs && transferTotal !== undefined) { const transactionPayload = payload as ISignedTransactionPayload; const transaction = transactionPayload.transaction; - const nestedTransactionId = - transaction.payload?.type === PayloadType.SignedTransaction - ? Utils.transactionId(transaction.payload as ISignedTransactionPayload) - : undefined; return ( @@ -43,16 +35,6 @@ const BlockPayloadSection: React.FC = ({ block, inputs
)} - {nestedTransactionId && ( -
-
-
Transaction
-
- -
-
-
- )} ); } else if (payload?.type === PayloadType.TaggedData) { From 0c009d48a12c8f6ec1cd3a3994fc1b004d575603 Mon Sep 17 00:00:00 2001 From: Branko Bosnic Date: Wed, 24 Jan 2024 13:14:18 +0100 Subject: [PATCH 15/15] fix: insantiate block using Plain Instance to a class in useBlock --- client/src/app/routes/nova/Block.tsx | 65 ++++++++--------------- client/src/helpers/nova/hooks/useBlock.ts | 4 +- 2 files changed, 26 insertions(+), 43 deletions(-) diff --git a/client/src/app/routes/nova/Block.tsx b/client/src/app/routes/nova/Block.tsx index bb66a4b55..25c53f73f 100644 --- a/client/src/app/routes/nova/Block.tsx +++ b/client/src/app/routes/nova/Block.tsx @@ -4,15 +4,7 @@ import mainHeaderMessage from "~assets/modals/stardust/block/main-header.json"; import metadataInfo from "~assets/modals/stardust/block/metadata.json"; import { useBlock } from "~helpers/nova/hooks/useBlock"; import NotFound from "../../components/NotFound"; -import { - BasicBlockBody, - BlockBody, - BlockBodyType, - PayloadType, - SignedTransactionPayload, - Utils, - ValidationBlockBody, -} from "@iota/sdk-wasm-nova/web"; +import { BasicBlockBody, PayloadType, SignedTransactionPayload, Utils, ValidationBlockBody } from "@iota/sdk-wasm-nova/web"; import Modal from "~/app/components/Modal"; import Spinner from "~/app/components/Spinner"; import TruncatedId from "~/app/components/stardust/TruncatedId"; @@ -28,6 +20,7 @@ import taggedDataPayloadInfo from "~assets/modals/stardust/block/tagged-data-pay import transactionPayloadInfo from "~assets/modals/stardust/block/transaction-payload.json"; import { useBlockMetadata } from "~/helpers/nova/hooks/useBlockMetadata"; import TransactionMetadataSection from "~/app/components/nova/block/section/TransactionMetadataSection"; + export interface BlockProps { /** * The network to lookup. @@ -51,14 +44,10 @@ const Block: React.FC> = ({ const [block, isLoading, blockError] = useBlock(network, blockId); const [blockMetadata] = useBlockMetadata(network, blockId); const [inputs, outputs, transferTotal] = useInputsAndOutputs(network, block); - const [blockBody, setBlockBody] = useState(); + const [blockBody, setBlockBody] = useState(); const [transactionId, setTransactionId] = useState(); const [pageTitle, setPageTitle] = useState("Block"); - function isBasicBlockBody(body: BlockBody): body is BasicBlockBody { - return body.type === BlockBodyType.Basic; - } - function updatePageTitle(type: PayloadType | undefined): void { let title = null; switch (type) { @@ -78,26 +67,18 @@ const Block: React.FC> = ({ } } useEffect(() => { - switch (block?.body?.type) { - case BlockBodyType.Basic: { - const body = block?.body as BasicBlockBody; - setBlockBody(body); - updatePageTitle(body.payload?.type); - if (body.payload?.type === PayloadType.SignedTransaction) { - const tsxId = Utils.transactionId(body.payload as SignedTransactionPayload); - setTransactionId(tsxId); - } - break; - } - case BlockBodyType.Validation: { - setPageTitle(`Validation ${pageTitle}`); - const body = block?.body as BasicBlockBody; - setBlockBody(body); - break; - } - default: { - break; + if (block?.isBasic()) { + const body = block.body.asBasic(); + setBlockBody(body); + updatePageTitle(body.payload?.type); + + if (body.payload?.type === PayloadType.SignedTransaction) { + const tsxId = Utils.transactionId(body.payload as SignedTransactionPayload); + setTransactionId(tsxId); } + } else { + setBlockBody(block?.body.asValidation()); + setPageTitle(`Validation ${pageTitle}`); } }, [block]); @@ -202,19 +183,19 @@ const Block: React.FC> = ({
)}
- {blockBody && !isBasicBlockBody(blockBody) && ( + {blockBody?.isValidation() && (
Highest supported protocol version
-
{blockBody.highestSupportedVersion}
+
{blockBody.asValidation().highestSupportedVersion}
)} - {blockBody && isBasicBlockBody(blockBody) && ( + {blockBody?.isBasic() && (
Max burned mana
-
{Number(blockBody.maxBurnedMana)}
+
{Number(blockBody.asBasic().maxBurnedMana)}
- {blockBody.payload?.type === PayloadType.SignedTransaction && transferTotal !== null && ( + {blockBody.asBasic().payload?.type === PayloadType.SignedTransaction && transferTotal !== null && (
Amount transacted
@@ -228,22 +209,22 @@ const Block: React.FC> = ({ { if (isMounted) { - setBlock(response.block ?? null); + const block = plainToInstance(Block, response.block) as unknown as Block; + setBlock(block ?? null); setError(response.error); } })