Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Basic support for Anchor address page #1093

Merged
merged 8 commits into from
Feb 7, 2024
11 changes: 11 additions & 0 deletions api/src/models/api/nova/IAnchorDetailsRequest.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/IAnchorDetailsResponse.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions api/src/routes/nova/anchor/get.ts
Original file line number Diff line number Diff line change
@@ -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<IAnchorDetailsResponse> {
const networkService = ServiceFactory.get<NetworkService>("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<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.anchorDetails(request.anchorId);
}
20 changes: 20 additions & 0 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<IAnchorDetailsResponse | undefined> {
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.
Expand Down
52 changes: 52 additions & 0 deletions client/src/app/components/nova/address/AnchorAddressView.tsx
Original file line number Diff line number Diff line change
@@ -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<AnchorAddressViewProps> = ({ anchorAddress }) => {
const { anchorAddressDetails, isAnchorDetailsLoading } = useAnchorAddressState(anchorAddress);
const isPageLoading = isAnchorDetailsLoading;

return (
<div className="address-page">
<div className="wrapper">
{anchorAddressDetails && (
<div className="inner">
<div className="addr--header">
<div className="row middle">
<h1>{anchorAddressDetails.typeLabel?.replace("Ed25519", "Address")}</h1>
</div>
{isPageLoading && <Spinner />}
</div>
<div className="section no-border-bottom padding-b-0">
<div className="section--header">
<div className="row middle">
<h2>General</h2>
</div>
</div>
<div className="general-content">
<div className="section--data">
<Bech32Address addressDetails={anchorAddressDetails} advancedMode={true} />
</div>
</div>
</div>
<div className="section no-border-bottom padding-b-0">
<div className="row middle">
<h2>Associated Outputs</h2>
</div>
<AssociatedOutputs addressDetails={anchorAddressDetails} />
</div>
</div>
)}
</div>
</div>
);
};

export default AnchorAddressView;
7 changes: 5 additions & 2 deletions client/src/app/routes/nova/AddressPage.tsx
Original file line number Diff line number Diff line change
@@ -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<RouteComponentProps<AddressRouteProps>> = ({
match: {
Expand All @@ -29,6 +30,8 @@ const AddressPage: React.FC<RouteComponentProps<AddressRouteProps>> = ({
return <AccountAddressView accountAddress={parsedAddress as AccountAddress} />;
case AddressType.Nft:
return <NftAddressView nftAddress={parsedAddress as NftAddress} />;
case AddressType.Anchor:
return <AnchorAddressView anchorAddress={parsedAddress as AnchorAddress} />;
default:
return (
<div className="address-page">
Expand Down
6 changes: 5 additions & 1 deletion client/src/helpers/nova/bech32AddressHelper.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -73,6 +75,8 @@ export class Bech32AddressHelper {
return "Account";
} else if (addressType === AddressType.Nft) {
return "NFT";
} else if (addressType === AddressType.Anchor) {
return "Anchor";
}
}
}
64 changes: 64 additions & 0 deletions client/src/helpers/nova/hooks/useAnchorAddressState.ts
Original file line number Diff line number Diff line change
@@ -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<AddressRouteProps>();
const { bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo);
const [state, setState] = useReducer<Reducer<IAnchorAddressState, Partial<IAnchorAddressState>>>(
(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,
};
};
48 changes: 48 additions & 0 deletions client/src/helpers/nova/hooks/useAnchorDetails.ts
Original file line number Diff line number Diff line change
@@ -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<NovaApiClient>(`api-client-${NOVA}`));
const [anchorOutput, setAnchorOutput] = useState<AnchorOutput | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(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 };
}
11 changes: 11 additions & 0 deletions client/src/models/api/nova/IAnchorDetailsRequest.ts
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 9 additions & 0 deletions client/src/models/api/nova/IAnchorDetailsResponse.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions client/src/services/nova/novaApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -76,6 +78,15 @@ export class NovaApiClient extends ApiClient {
return this.callApi<unknown, INftDetailsResponse>(`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<IAnchorDetailsResponse> {
return this.callApi<unknown, IAnchorDetailsResponse>(`nova/anchor/${request.network}/${request.anchorId}`, "get");
}

/**
* Get the associated outputs.
* @param request The request to send.
Expand Down
Loading