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 NFT address page #1091

Merged
merged 6 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading