From 90a36e6c2a3bfa748777e57d51502ad022645fec Mon Sep 17 00:00:00 2001 From: JCNoguera Date: Tue, 20 Feb 2024 18:16:37 +0100 Subject: [PATCH] feat: add slot page --- api/src/models/api/nova/ISlotRequest.ts | 11 +++ api/src/models/api/nova/ISlotResponse.ts | 8 ++ api/src/routes.ts | 1 + api/src/routes/nova/slot/get.ts | 30 +++++++ api/src/services/nova/novaApiService.ts | 11 +++ client/src/app/routes.tsx | 2 + client/src/app/routes/nova/SlotPage.scss | 86 ++++++++++++++++++++ client/src/app/routes/nova/SlotPage.tsx | 90 +++++++++++++++++++++ client/src/helpers/nova/hooks/useSlot.ts | 54 +++++++++++++ client/src/models/api/nova/ISlotRequest.ts | 11 +++ client/src/models/api/nova/ISlotResponse.ts | 6 ++ client/src/services/nova/novaApiClient.ts | 11 +++ 12 files changed, 321 insertions(+) create mode 100644 api/src/models/api/nova/ISlotRequest.ts create mode 100644 api/src/models/api/nova/ISlotResponse.ts create mode 100644 api/src/routes/nova/slot/get.ts create mode 100644 client/src/app/routes/nova/SlotPage.scss create mode 100644 client/src/app/routes/nova/SlotPage.tsx create mode 100644 client/src/helpers/nova/hooks/useSlot.ts create mode 100644 client/src/models/api/nova/ISlotRequest.ts create mode 100644 client/src/models/api/nova/ISlotResponse.ts diff --git a/api/src/models/api/nova/ISlotRequest.ts b/api/src/models/api/nova/ISlotRequest.ts new file mode 100644 index 000000000..557fb3337 --- /dev/null +++ b/api/src/models/api/nova/ISlotRequest.ts @@ -0,0 +1,11 @@ +export interface ISlotRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The slot index to get the details for. + */ + slotIndex: string; +} diff --git a/api/src/models/api/nova/ISlotResponse.ts b/api/src/models/api/nova/ISlotResponse.ts new file mode 100644 index 000000000..455990095 --- /dev/null +++ b/api/src/models/api/nova/ISlotResponse.ts @@ -0,0 +1,8 @@ +import { SlotCommitment } from "@iota/sdk-nova"; + +export interface ISlotResponse { + /** + * The deserialized slot. + */ + slot?: SlotCommitment; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index 4fbceb165..33af036bf 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -244,4 +244,5 @@ export const routes: IRoute[] = [ }, { 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" }, + { path: "/nova/slot/:network/:slotIndex", method: "get", folder: "nova/slot", func: "get" }, ]; diff --git a/api/src/routes/nova/slot/get.ts b/api/src/routes/nova/slot/get.ts new file mode 100644 index 000000000..40339920f --- /dev/null +++ b/api/src/routes/nova/slot/get.ts @@ -0,0 +1,30 @@ +import { ServiceFactory } from "../../../factories/serviceFactory"; +import { ISlotRequest } from "../../../models/api/nova/ISlotRequest"; +import { ISlotResponse } from "../../../models/api/nova/ISlotResponse"; +import { IConfiguration } from "../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../models/db/protocolVersion"; +import { NetworkService } from "../../../services/networkService"; +import { NovaApiService } from "../../../services/nova/novaApiService"; +import { ValidationHelper } from "../../../utils/validationHelper"; + +/** + * Fetch the block from the network. + * @param _ The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(_: IConfiguration, request: ISlotRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.numberFromString(request.slotIndex, "slotIndex"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.getSlotCommitment(Number(request.slotIndex)); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 09d70ff27..b00d45de0 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -15,6 +15,7 @@ import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse"; import { ISearchResponse } from "../../models/api/nova/ISearchResponse"; +import { ISlotResponse } from "../../models/api/nova/ISlotResponse"; import { INetwork } from "../../models/db/INetwork"; import { HexHelper } from "../../utils/hexHelper"; import { SearchExecutor } from "../../utils/nova/searchExecutor"; @@ -261,6 +262,16 @@ export class NovaApiService { return manaRewardsResponse ? { outputId, manaRewards: manaRewardsResponse } : { outputId, message: "Rewards data not found" }; } + public async getSlotCommitment(slotIndex: number): Promise { + try { + const slot = await this.client.getCommitmentByIndex(slotIndex); + + return { slot }; + } catch (e) { + logger.error(`Failed fetching slot with slot index ${slotIndex}. Cause: ${e}`); + } + } + /** * Find item on the stardust network. * @param query The query to use for finding items. diff --git a/client/src/app/routes.tsx b/client/src/app/routes.tsx index cd52d8ed5..f0633a9e1 100644 --- a/client/src/app/routes.tsx +++ b/client/src/app/routes.tsx @@ -37,6 +37,7 @@ import StardustOutputPage from "./routes/stardust/OutputPage"; import NovaBlockPage from "./routes/nova/Block"; import NovaOutputPage from "./routes/nova/OutputPage"; import NovaSearch from "./routes/nova/Search"; +import NovaSlotPage from "./routes/nova/SlotPage"; import StardustSearch from "./routes/stardust/Search"; import StardustStatisticsPage from "./routes/stardust/statistics/StatisticsPage"; import StardustTransactionPage from "./routes/stardust/TransactionPage"; @@ -178,6 +179,7 @@ const buildAppRoutes = (protocolVersion: string, withNetworkContext: (wrappedCom , , , + , ]; return ( diff --git a/client/src/app/routes/nova/SlotPage.scss b/client/src/app/routes/nova/SlotPage.scss new file mode 100644 index 000000000..2d4b2e13f --- /dev/null +++ b/client/src/app/routes/nova/SlotPage.scss @@ -0,0 +1,86 @@ +@import "../../../scss/fonts"; +@import "../../../scss/mixins"; +@import "../../../scss/media-queries"; +@import "../../../scss/variables"; + +.slot-page { + display: flex; + flex-direction: column; + + .wrapper { + display: flex; + justify-content: center; + + .inner { + display: flex; + flex: 1; + flex-direction: column; + max-width: $desktop-width; + margin: 40px 25px; + + @include desktop-down { + flex: unset; + width: 100%; + max-width: 100%; + margin: 40px 24px; + padding-right: 24px; + padding-left: 24px; + + > .row { + flex-direction: column; + } + } + + @include tablet-down { + margin: 28px 0; + } + + .slot-page--header { + display: flex; + align-items: center; + justify-content: space-between; + } + + .section { + padding-top: 44px; + + .section--header { + margin-top: 44px; + } + + .card--content__output { + margin-top: 20px; + } + } + } + } + + .tooltip { + .children { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + .tooltip__special { + background-color: #fff4df; + border-radius: 4px; + padding: 0 4px; + font-weight: 400; + } + + .wrap { + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + width: 170px; + + .arrow { + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + } + } + } +} diff --git a/client/src/app/routes/nova/SlotPage.tsx b/client/src/app/routes/nova/SlotPage.tsx new file mode 100644 index 000000000..78156f776 --- /dev/null +++ b/client/src/app/routes/nova/SlotPage.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import Modal from "~/app/components/Modal"; +import { ModalData } from "~/app/components/ModalProps"; +import { RouteComponentProps } from "react-router-dom"; +import TruncatedId from "~/app/components/stardust/TruncatedId"; +import classNames from "classnames"; +import useSlotData from "~/helpers/nova/hooks/useSlot"; +import "./SlotPage.scss"; + +interface SlotPageProps { + network: string; + slotIndex: string; +} + +export default function SlotPage({ + match: { + params: { network, slotIndex }, + }, +}: RouteComponentProps): React.JSX.Element { + const { slotCommitment } = useSlotData(network, slotIndex); + + const message: ModalData = { + title: "Slot Page", + description: "

Slot Information here

", + }; + + const dataRows: IDataRow[] = [ + { + label: "Slot Index", + value: slotCommitment?.slot, + highlighted: true, + }, + { + label: "RMC", + value: slotCommitment?.referenceManaCost.toString(), + }, + ]; + + return ( +
+
+
+
+
+

Slot

+ +
+
+
+
+
+

General

+
+
+ {dataRows.map((dataRow, index) => { + if (dataRow.value || dataRow.truncatedId) { + return ; + } + })} +
+
+
+
+ ); +} + +interface IDataRow { + label: string; + value?: string | number; + highlighted?: boolean; + truncatedId?: { + id: string; + link?: string; + showCopyButton?: boolean; + }; +} +const DataRow = ({ label, value, truncatedId, highlighted }: IDataRow) => { + return ( +
+
{label}
+
+ {truncatedId ? ( + + ) : ( + value + )} +
+
+ ); +}; diff --git a/client/src/helpers/nova/hooks/useSlot.ts b/client/src/helpers/nova/hooks/useSlot.ts new file mode 100644 index 000000000..c3b5ee496 --- /dev/null +++ b/client/src/helpers/nova/hooks/useSlot.ts @@ -0,0 +1,54 @@ +import { SlotCommitment } from "@iota/sdk-wasm-nova/web"; +import { plainToInstance } from "class-transformer"; +import { useEffect, useState } from "react"; +import { ServiceFactory } from "~/factories/serviceFactory"; +import { useIsMounted } from "~/helpers/hooks/useIsMounted"; +import { NOVA } from "~/models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; + +interface IUseSlotData { + slotCommitment: SlotCommitment | null; + error: string | undefined; + isLoading: boolean; +} + +export default function useSlotData(network: string, slotIndex: string): IUseSlotData { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [slotCommitment, setSlotCommitment] = useState(null); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + setSlotCommitment(null); + if (!slotCommitment) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .getSlotCommitment({ + network, + slotIndex, + }) + .then((response) => { + if (isMounted) { + const slot = plainToInstance(SlotCommitment, response.slot) as unknown as SlotCommitment; + setSlotCommitment(slot); + setError(response.error); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, slotIndex]); + + return { + slotCommitment, + error, + isLoading, + }; +} diff --git a/client/src/models/api/nova/ISlotRequest.ts b/client/src/models/api/nova/ISlotRequest.ts new file mode 100644 index 000000000..b00482593 --- /dev/null +++ b/client/src/models/api/nova/ISlotRequest.ts @@ -0,0 +1,11 @@ +export interface ISlotRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The slot index to get the commitment for. + */ + slotIndex: string; +} diff --git a/client/src/models/api/nova/ISlotResponse.ts b/client/src/models/api/nova/ISlotResponse.ts new file mode 100644 index 000000000..dc7e83cb4 --- /dev/null +++ b/client/src/models/api/nova/ISlotResponse.ts @@ -0,0 +1,6 @@ +import { SlotCommitment } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "../IResponse"; + +export interface ISlotResponse extends IResponse { + slot: SlotCommitment; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index 0ad378576..66aaf87f5 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -29,6 +29,8 @@ import { IAddressDetailsRequest } from "~/models/api/nova/address/IAddressDetail import { IAddressDetailsResponse } from "~/models/api/nova/address/IAddressDetailsResponse"; import { IFoundriesResponse } from "~/models/api/nova/foundry/IFoundriesResponse"; import { IFoundriesRequest } from "~/models/api/nova/foundry/IFoundriesRequest"; +import { ISlotRequest } from "~/models/api/nova/ISlotRequest"; +import { ISlotResponse } from "~/models/api/nova/ISlotResponse"; /** * Class to handle api communications on nova. @@ -155,6 +157,15 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/output/rewards/${request.network}/${request.outputId}`, "get"); } + /** + * Get the slot commitment. + * @param request The request to send. + * @returns The response from the request. + */ + public async getSlotCommitment(request: ISlotRequest): Promise { + return this.callApi(`nova/slot/${request.network}/${request.slotIndex}`, "get"); + } + /** * Get the stats. * @param request The request to send.