Skip to content

Commit

Permalink
feat: Basic support for Anchor address page (#1093)
Browse files Browse the repository at this point in the history
* chore: Extract AddressNotFound to own component

* feat: Make AddressPage.tsx an entrypoint for address pages. Refactor useAddressPageState to be Account address specific. Add AccountAddressView.

* feat: Add support for Ed25519 address page (state hook and tsx component)

* fix: Fix OutputPage for Nft and Account outputs

* feat: Support Nft address page (basic). Add custom hooks for nft address page and load nft output details.

* feat: Support Anchor address page (basic). Add custom hooks for anchor address page and load anchor output details.

* feat: Add Anchor address support in bech32AddressHelper
  • Loading branch information
msarcev authored Feb 7, 2024
1 parent f6de9d3 commit ac0dac8
Show file tree
Hide file tree
Showing 13 changed files with 277 additions and 3 deletions.
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

0 comments on commit ac0dac8

Please sign in to comment.