Skip to content

Commit

Permalink
feat: Basic support for NFT address page (#1091)
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.
  • Loading branch information
msarcev authored Feb 7, 2024
1 parent 9b6ad87 commit f6de9d3
Show file tree
Hide file tree
Showing 20 changed files with 297 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface IAccountRequest {
export interface IAccountDetailsRequest {
/**
* The network to search on.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import { OutputResponse } from "@iota/sdk-nova";
import { IResponse } from "./IResponse";

export interface IAccountResponse extends IResponse {
export interface IAccountDetailsResponse extends IResponse {
/**
* The account details response.
*/
accountDetails?: OutputResponse;
accountOutputDetails?: OutputResponse;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/INftDetailsRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface INftDetailsRequest {
/**
* The network to search on.
*/
network: string;

/**
* The nft id to get the nft output details for.
*/
nftId: string;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/INftDetailsResponse.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 INftDetailsResponse extends IResponse {
/**
* The nft output details response.
*/
nftOutputDetails?: OutputResponse;
}
1 change: 1 addition & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export const routes: IRoute[] = [
{ path: "/nova/output/:network/:outputId", method: "get", folder: "nova/output", func: "get" },
{ 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/output/associated/:network/:address",
method: "post",
Expand Down
6 changes: 3 additions & 3 deletions api/src/routes/nova/account/get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ServiceFactory } from "../../../factories/serviceFactory";
import { IAccountRequest } from "../../../models/api/nova/IAccountRequest";
import { IAccountResponse } from "../../../models/api/nova/IAccountResponse";
import { IAccountDetailsRequest } from "../../../models/api/nova/IAccountDetailsRequest";
import { IAccountDetailsResponse } from "../../../models/api/nova/IAccountDetailsResponse";
import { IConfiguration } from "../../../models/configuration/IConfiguration";
import { NOVA } from "../../../models/db/protocolVersion";
import { NetworkService } from "../../../services/networkService";
Expand All @@ -13,7 +13,7 @@ import { ValidationHelper } from "../../../utils/validationHelper";
* @param request The request.
* @returns The response.
*/
export async function get(config: IConfiguration, request: IAccountRequest): Promise<IAccountResponse> {
export async function get(config: IConfiguration, request: IAccountDetailsRequest): Promise<IAccountDetailsResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
Expand Down
29 changes: 29 additions & 0 deletions api/src/routes/nova/nft/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ServiceFactory } from "../../../factories/serviceFactory";
import { INftDetailsRequest } from "../../../models/api/nova/INftDetailsRequest";
import { INftDetailsResponse } from "../../../models/api/nova/INftDetailsResponse";
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 nft output details by Nft id
* @param config The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(config: IConfiguration, request: INftDetailsRequest): Promise<INftDetailsResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.string(request.nftId, "nftId");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}
const novaApiService = ServiceFactory.get<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.nftDetails(request.nftId);
}
32 changes: 26 additions & 6 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import { Client } from "@iota/sdk-nova";
import { ServiceFactory } from "../../factories/serviceFactory";
import logger from "../../logger";
import { IAccountResponse } from "../../models/api/nova/IAccountResponse";
import { IAccountDetailsResponse } from "../../models/api/nova/IAccountDetailsResponse";
import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse";
import { IBlockResponse } from "../../models/api/nova/IBlockResponse";
import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse";
import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse";
import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse";
import { INetwork } from "../../models/db/INetwork";
Expand Down Expand Up @@ -86,24 +87,43 @@ export class NovaApiService {
}

/**
* Get the account details.
* @param accountId The accountId to get the details for.
* @returns The account details.
* Get the account output details.
* @param accountId The accountId to get the output details for.
* @returns The account output details.
*/
public async accountDetails(accountId: string): Promise<IAccountResponse | undefined> {
public async accountDetails(accountId: string): Promise<IAccountDetailsResponse | undefined> {
try {
const accountOutputId = await this.client.accountOutputId(accountId);

if (accountOutputId) {
const outputResponse = await this.outputDetails(accountOutputId);

return outputResponse.error ? { error: outputResponse.error } : { accountDetails: outputResponse.output };
return outputResponse.error ? { error: outputResponse.error } : { accountOutputDetails: outputResponse.output };
}
} catch {
return { message: "Account output not found" };
}
}

/**
* Get the nft output details.
* @param nftId The nftId to get the output details for.
* @returns The nft output details.
*/
public async nftDetails(nftId: string): Promise<INftDetailsResponse | undefined> {
try {
const nftOutputId = await this.client.nftOutputId(nftId);

if (nftOutputId) {
const outputResponse = await this.outputDetails(nftOutputId);

return outputResponse.error ? { error: outputResponse.error } : { nftOutputDetails: outputResponse.output };
}
} catch {
return { message: "Nft output not found" };
}
}

/**
* Get the output mana rewards.
* @param outputId The outputId to get the rewards for.
Expand Down
10 changes: 5 additions & 5 deletions client/src/app/components/nova/OutputView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import {
SimpleTokenScheme,
DelegationOutput,
AddressType,
Utils,
} from "@iota/sdk-wasm-nova/web";
import UnlockConditionView from "./UnlockConditionView";
import CopyButton from "../CopyButton";
import { Link } from "react-router-dom";
import { useNetworkInfoNova } from "~/helpers/nova/networkInfo";
import FeatureView from "./FeaturesView";
import TruncatedId from "../stardust/TruncatedId";
import { TransactionsHelper } from "~/helpers/stardust/transactionsHelper";
import { Bech32AddressHelper } from "~/helpers/nova/bech32AddressHelper";
import "./OutputView.scss";

Expand Down Expand Up @@ -216,16 +216,16 @@ const OutputView: React.FC<OutputViewProps> = ({ outputId, output, showCopyAmoun
);
};

function buildAddressForAliasOrNft(outputId: string, output: Output, bech32Hrp: string) {
function buildAddressForAliasOrNft(outputId: string, output: Output, bech32Hrp: string): string {
let address: string = "";
let addressType: number = 0;

if (output.type === OutputType.Account) {
const aliasId = TransactionsHelper.buildIdHashForNft((output as AccountOutput).accountId, outputId);
address = aliasId;
const accountId = Utils.computeAccountId(outputId);
address = accountId;
addressType = AddressType.Account;
} else if (output.type === OutputType.Nft) {
const nftId = TransactionsHelper.buildIdHashForAlias((output as NftOutput).nftId, outputId);
const nftId = Utils.computeNftId(outputId);
address = nftId;
addressType = AddressType.Nft;
}
Expand Down
52 changes: 52 additions & 0 deletions client/src/app/components/nova/address/NftAddressView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NftAddress } from "@iota/sdk-wasm-nova/web";
import React from "react";
import { useNftAddressState } from "~/helpers/nova/hooks/useNftAddressState";
import Spinner from "../../Spinner";
import Bech32Address from "../../stardust/address/Bech32Address";
import AssociatedOutputs from "./section/association/AssociatedOutputs";

interface NftAddressViewProps {
nftAddress: NftAddress;
}

const NftAddressView: React.FC<NftAddressViewProps> = ({ nftAddress }) => {
const { nftAddressDetails, isNftDetailsLoading } = useNftAddressState(nftAddress);
const isPageLoading = isNftDetailsLoading;

return (
<div className="address-page">
<div className="wrapper">
{nftAddressDetails && (
<div className="inner">
<div className="addr--header">
<div className="row middle">
<h1>{nftAddressDetails.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={nftAddressDetails} 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={nftAddressDetails} />
</div>
</div>
)}
</div>
</div>
);
};

export default NftAddressView;
5 changes: 4 additions & 1 deletion client/src/app/routes/nova/AddressPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { AccountAddress, Address, AddressType, Ed25519Address, Utils } from "@iota/sdk-wasm-nova/web";
import { AccountAddress, Address, AddressType, 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";

const AddressPage: React.FC<RouteComponentProps<AddressRouteProps>> = ({
match: {
Expand All @@ -26,6 +27,8 @@ const AddressPage: React.FC<RouteComponentProps<AddressRouteProps>> = ({
return <Ed25519AddressView ed25519Address={parsedAddress as Ed25519Address} />;
case AddressType.Account:
return <AccountAddressView accountAddress={parsedAddress as AccountAddress} />;
case AddressType.Nft:
return <NftAddressView nftAddress={parsedAddress as NftAddress} />;
default:
return (
<div className="address-page">
Expand Down
2 changes: 1 addition & 1 deletion client/src/helpers/nova/bech32AddressHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class Bech32AddressHelper {

return {
bech32,
hex,
hex: hex ? HexHelper.addPrefix(hex) : hex,
type,
typeLabel: Bech32AddressHelper.typeLabel(type),
};
Expand Down
2 changes: 1 addition & 1 deletion client/src/helpers/nova/hooks/useAccountDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function useAccountDetails(network: string, accountId: string | null): {
})
.then((response) => {
if (!response?.error && isMounted) {
const output = response.accountDetails?.output as AccountOutput;
const output = response.accountOutputDetails?.output as AccountOutput;

setAccountOutput(output);
}
Expand Down
64 changes: 64 additions & 0 deletions client/src/helpers/nova/hooks/useNftAddressState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Reducer, useEffect, useReducer } from "react";
import { NftAddress, NftOutput } from "@iota/sdk-wasm-nova/web";
import { IBech32AddressDetails } from "~/models/api/IBech32AddressDetails";
import { useNftDetails } from "./useNftDetails";
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 INftAddressState {
nftAddressDetails: IBech32AddressDetails | null;
nftOutput: NftOutput | null;
isNftDetailsLoading: boolean;
}

const initialState = {
nftAddressDetails: null,
nftOutput: null,
isNftDetailsLoading: true,
};

/**
* Route Location Props
*/
interface IAddressPageLocationProps {
addressDetails: IBech32AddressDetails;
}

export const useNftAddressState = (address: NftAddress): INftAddressState => {
const location = useLocation();
const { network } = useParams<AddressRouteProps>();
const { bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo);
const [state, setState] = useReducer<Reducer<INftAddressState, Partial<INftAddressState>>>(
(currentState, newState) => ({ ...currentState, ...newState }),
initialState,
);

const { nftOutput, isLoading: isNftDetailsLoading } = useNftDetails(network, address.nftId);

useEffect(() => {
const locationState = location.state as IAddressPageLocationProps;
const { addressDetails } = locationState?.addressDetails
? locationState
: { addressDetails: Bech32AddressHelper.buildAddress(bech32Hrp, address) };

setState({
...initialState,
nftAddressDetails: addressDetails,
});
}, []);

useEffect(() => {
setState({
nftOutput,
isNftDetailsLoading,
});
}, [nftOutput, isNftDetailsLoading]);

return {
nftAddressDetails: state.nftAddressDetails,
nftOutput: state.nftOutput,
isNftDetailsLoading: state.isNftDetailsLoading,
};
};
Loading

0 comments on commit f6de9d3

Please sign in to comment.