diff --git a/api/src/models/api/nova/IAnchorDetailsRequest.ts b/api/src/models/api/nova/IAnchorDetailsRequest.ts new file mode 100644 index 000000000..9236046dc --- /dev/null +++ b/api/src/models/api/nova/IAnchorDetailsRequest.ts @@ -0,0 +1,11 @@ +export interface IAnchorDetailsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The anchor id to get the anchor output details for. + */ + anchorId: string; +} diff --git a/api/src/models/api/nova/IAnchorDetailsResponse.ts b/api/src/models/api/nova/IAnchorDetailsResponse.ts new file mode 100644 index 000000000..db961a868 --- /dev/null +++ b/api/src/models/api/nova/IAnchorDetailsResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { OutputResponse } from "@iota/sdk-nova"; +import { IResponse } from "./IResponse"; + +export interface IAnchorDetailsResponse extends IResponse { + /** + * The anchor details response. + */ + anchorOutputDetails?: OutputResponse; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index c3ec04325..d8b1ade11 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -206,6 +206,7 @@ export const routes: IRoute[] = [ { path: "/nova/output/rewards/:network/:outputId", method: "get", folder: "nova/output/rewards", func: "get" }, { path: "/nova/account/:network/:accountId", method: "get", folder: "nova/account", func: "get" }, { path: "/nova/nft/:network/:nftId", method: "get", folder: "nova/nft", func: "get" }, + { path: "/nova/anchor/:network/:anchorId", method: "get", folder: "nova/anchor", func: "get" }, { path: "/nova/output/associated/:network/:address", method: "post", diff --git a/api/src/routes/nova/anchor/get.ts b/api/src/routes/nova/anchor/get.ts new file mode 100644 index 000000000..17e787999 --- /dev/null +++ b/api/src/routes/nova/anchor/get.ts @@ -0,0 +1,29 @@ +import { ServiceFactory } from "../../../factories/serviceFactory"; +import { IAnchorDetailsRequest } from "../../../models/api/nova/IAnchorDetailsRequest"; +import { IAnchorDetailsResponse } from "../../../models/api/nova/IAnchorDetailsResponse"; +import { IConfiguration } from "../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../models/db/protocolVersion"; +import { NetworkService } from "../../../services/networkService"; +import { NovaApiService } from "../../../services/nova/novaApiService"; +import { ValidationHelper } from "../../../utils/validationHelper"; + +/** + * Get anchor output details by Anchor id + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: IAnchorDetailsRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.anchorId, "anchorId"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.anchorDetails(request.anchorId); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 549e5001a..b3388a8db 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -4,6 +4,7 @@ import { Client } from "@iota/sdk-nova"; import { ServiceFactory } from "../../factories/serviceFactory"; import logger from "../../logger"; import { IAccountDetailsResponse } from "../../models/api/nova/IAccountDetailsResponse"; +import { IAnchorDetailsResponse } from "../../models/api/nova/IAnchorDetailsResponse"; import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse"; import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse"; @@ -124,6 +125,25 @@ export class NovaApiService { } } + /** + * Get the anchor output details. + * @param anchorId The anchorId to get the output details for. + * @returns The anchor output details. + */ + public async anchorDetails(anchorId: string): Promise { + try { + const anchorOutputId = await this.client.anchorOutputId(anchorId); + + if (anchorOutputId) { + const outputResponse = await this.outputDetails(anchorOutputId); + + return outputResponse.error ? { error: outputResponse.error } : { anchorOutputDetails: outputResponse.output }; + } + } catch { + return { message: "Anchor output not found" }; + } + } + /** * Get the output mana rewards. * @param outputId The outputId to get the rewards for. diff --git a/client/src/app/components/nova/address/AnchorAddressView.tsx b/client/src/app/components/nova/address/AnchorAddressView.tsx new file mode 100644 index 000000000..622c4d696 --- /dev/null +++ b/client/src/app/components/nova/address/AnchorAddressView.tsx @@ -0,0 +1,52 @@ +import { AnchorAddress } from "@iota/sdk-wasm-nova/web"; +import React from "react"; +import { useAnchorAddressState } from "~/helpers/nova/hooks/useAnchorAddressState"; +import Spinner from "../../Spinner"; +import Bech32Address from "../../stardust/address/Bech32Address"; +import AssociatedOutputs from "./section/association/AssociatedOutputs"; + +interface AnchorAddressViewProps { + anchorAddress: AnchorAddress; +} + +const AnchorAddressView: React.FC = ({ anchorAddress }) => { + const { anchorAddressDetails, isAnchorDetailsLoading } = useAnchorAddressState(anchorAddress); + const isPageLoading = isAnchorDetailsLoading; + + return ( +
+
+ {anchorAddressDetails && ( +
+
+
+

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

+
+ {isPageLoading && } +
+
+
+
+

General

+
+
+
+
+ +
+
+
+
+
+

Associated Outputs

+
+ +
+
+ )} +
+
+ ); +}; + +export default AnchorAddressView; diff --git a/client/src/app/routes/nova/AddressPage.tsx b/client/src/app/routes/nova/AddressPage.tsx index 0abda5cfc..7236ad519 100644 --- a/client/src/app/routes/nova/AddressPage.tsx +++ b/client/src/app/routes/nova/AddressPage.tsx @@ -1,12 +1,13 @@ -import { AccountAddress, Address, AddressType, Ed25519Address, NftAddress, Utils } from "@iota/sdk-wasm-nova/web"; +import { AccountAddress, Address, AddressType, AnchorAddress, Ed25519Address, NftAddress, Utils } from "@iota/sdk-wasm-nova/web"; import React from "react"; import { RouteComponentProps } from "react-router-dom"; import AddressNotFoundPage from "~/app/components/nova/address/AddressNotFoundPage"; import { AddressRouteProps } from "../AddressRouteProps"; import AccountAddressView from "~/app/components/nova/address/AccountAddressView"; import Ed25519AddressView from "~/app/components/nova/address/Ed25519AddressView"; -import "./AddressPage.scss"; import NftAddressView from "~/app/components/nova/address/NftAddressView"; +import AnchorAddressView from "~/app/components/nova/address/AnchorAddressView"; +import "./AddressPage.scss"; const AddressPage: React.FC> = ({ match: { @@ -29,6 +30,8 @@ const AddressPage: React.FC> = ({ return ; case AddressType.Nft: return ; + case AddressType.Anchor: + return ; default: return (
diff --git a/client/src/helpers/nova/bech32AddressHelper.ts b/client/src/helpers/nova/bech32AddressHelper.ts index f1e5f3b1e..e178078c6 100644 --- a/client/src/helpers/nova/bech32AddressHelper.ts +++ b/client/src/helpers/nova/bech32AddressHelper.ts @@ -1,5 +1,5 @@ import { Bech32Helper } from "@iota/iota.js"; -import { Address, AddressType, AccountAddress, Ed25519Address, NftAddress } from "@iota/sdk-wasm-nova/web"; +import { Address, AddressType, AccountAddress, Ed25519Address, NftAddress, AnchorAddress } from "@iota/sdk-wasm-nova/web"; import { Converter } from "../stardust/convertUtils"; import { HexHelper } from "../stardust/hexHelper"; import { IBech32AddressDetails } from "~models/api/IBech32AddressDetails"; @@ -56,6 +56,8 @@ export class Bech32AddressHelper { hex = HexHelper.stripPrefix((address as AccountAddress).accountId); } else if (address.type === AddressType.Nft) { hex = HexHelper.stripPrefix((address as NftAddress).nftId); + } else if (address.type === AddressType.Anchor) { + hex = HexHelper.stripPrefix((address as AnchorAddress).anchorId); } return this.buildAddressFromString(hrp, hex, address.type); @@ -73,6 +75,8 @@ export class Bech32AddressHelper { return "Account"; } else if (addressType === AddressType.Nft) { return "NFT"; + } else if (addressType === AddressType.Anchor) { + return "Anchor"; } } } diff --git a/client/src/helpers/nova/hooks/useAnchorAddressState.ts b/client/src/helpers/nova/hooks/useAnchorAddressState.ts new file mode 100644 index 000000000..dc2ff3c73 --- /dev/null +++ b/client/src/helpers/nova/hooks/useAnchorAddressState.ts @@ -0,0 +1,64 @@ +import { Reducer, useEffect, useReducer } from "react"; +import { AnchorAddress, AnchorOutput } from "@iota/sdk-wasm-nova/web"; +import { IBech32AddressDetails } from "~/models/api/IBech32AddressDetails"; +import { useAnchorDetails } from "./useAnchorDetails"; +import { useLocation, useParams } from "react-router-dom"; +import { AddressRouteProps } from "~/app/routes/AddressRouteProps"; +import { useNetworkInfoNova } from "../networkInfo"; +import { Bech32AddressHelper } from "~/helpers/nova/bech32AddressHelper"; + +export interface IAnchorAddressState { + anchorAddressDetails: IBech32AddressDetails | null; + anchorOutput: AnchorOutput | null; + isAnchorDetailsLoading: boolean; +} + +const initialState = { + anchorAddressDetails: null, + anchorOutput: null, + isAnchorDetailsLoading: true, +}; + +/** + * Route Location Props + */ +interface IAddressPageLocationProps { + addressDetails: IBech32AddressDetails; +} + +export const useAnchorAddressState = (address: AnchorAddress): IAnchorAddressState => { + const location = useLocation(); + const { network } = useParams(); + const { bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo); + const [state, setState] = useReducer>>( + (currentState, newState) => ({ ...currentState, ...newState }), + initialState, + ); + + const { anchorOutput, isLoading: isAnchorDetailsLoading } = useAnchorDetails(network, address.anchorId); + + useEffect(() => { + const locationState = location.state as IAddressPageLocationProps; + const { addressDetails } = locationState?.addressDetails + ? locationState + : { addressDetails: Bech32AddressHelper.buildAddress(bech32Hrp, address) }; + + setState({ + ...initialState, + anchorAddressDetails: addressDetails, + }); + }, []); + + useEffect(() => { + setState({ + anchorOutput, + isAnchorDetailsLoading, + }); + }, [anchorOutput, isAnchorDetailsLoading]); + + return { + anchorAddressDetails: state.anchorAddressDetails, + anchorOutput: state.anchorOutput, + isAnchorDetailsLoading: state.isAnchorDetailsLoading, + }; +}; diff --git a/client/src/helpers/nova/hooks/useAnchorDetails.ts b/client/src/helpers/nova/hooks/useAnchorDetails.ts new file mode 100644 index 000000000..3c88c281a --- /dev/null +++ b/client/src/helpers/nova/hooks/useAnchorDetails.ts @@ -0,0 +1,48 @@ +import { AnchorOutput } 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 anchor output details + * @param network The Network in context + * @param anchorID The anchor id + * @returns The output response and loading bool. + */ +export function useAnchorDetails(network: string, anchorId: string | null): { anchorOutput: AnchorOutput | null; isLoading: boolean } { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [anchorOutput, setAnchorOutput] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (anchorId) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .anchorDetails({ + network, + anchorId: HexHelper.addPrefix(anchorId), + }) + .then((response) => { + if (!response?.error && isMounted) { + const output = response.anchorOutputDetails?.output as AnchorOutput; + + setAnchorOutput(output); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, anchorId]); + + return { anchorOutput, isLoading }; +} diff --git a/client/src/models/api/nova/IAnchorDetailsRequest.ts b/client/src/models/api/nova/IAnchorDetailsRequest.ts new file mode 100644 index 000000000..9236046dc --- /dev/null +++ b/client/src/models/api/nova/IAnchorDetailsRequest.ts @@ -0,0 +1,11 @@ +export interface IAnchorDetailsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The anchor id to get the anchor output details for. + */ + anchorId: string; +} diff --git a/client/src/models/api/nova/IAnchorDetailsResponse.ts b/client/src/models/api/nova/IAnchorDetailsResponse.ts new file mode 100644 index 000000000..436f607af --- /dev/null +++ b/client/src/models/api/nova/IAnchorDetailsResponse.ts @@ -0,0 +1,9 @@ +import { OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "../IResponse"; + +export interface IAnchorDetailsResponse extends IResponse { + /** + * The anchor details response. + */ + anchorOutputDetails?: OutputResponse; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index fad3bce73..59e1dbee8 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -17,6 +17,8 @@ import { IStatsGetRequest } from "~/models/api/stats/IStatsGetRequest"; import { IStatsGetResponse } from "~/models/api/stats/IStatsGetResponse"; import { INftDetailsRequest } from "~/models/api/nova/INftDetailsRequest"; import { INftDetailsResponse } from "~/models/api/nova/INftDetailsResponse"; +import { IAnchorDetailsRequest } from "~/models/api/nova/IAnchorDetailsRequest"; +import { IAnchorDetailsResponse } from "~/models/api/nova/IAnchorDetailsResponse"; /** * Class to handle api communications on nova. @@ -76,6 +78,15 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/nft/${request.network}/${request.nftId}`, "get"); } + /** + * Get the anchor output details. + * @param request The request to send. + * @returns The response from the request. + */ + public async anchorDetails(request: IAnchorDetailsRequest): Promise { + return this.callApi(`nova/anchor/${request.network}/${request.anchorId}`, "get"); + } + /** * Get the associated outputs. * @param request The request to send.