Skip to content

Commit

Permalink
feat: Add Native tokens Tab to address pages (#1119)
Browse files Browse the repository at this point in the history
* feat: add output tab to address page

* fix: add components to display native tokens

* Remove unused code in AddressPageTabbedSections component

* remove unused interfaces

* Update address states and Add basic outputs API endpoint

* Add eslint-disable for unsafe return in novaApiService.ts

* Add Foundries tab to Accound address page (#1134)

* fix: validation check in foundries endpoint

* fix: component imports

* fix: imports

* fix: select first tab on address page

---------

Co-authored-by: Mario <[email protected]>
  • Loading branch information
brancoder and msarcev authored Feb 18, 2024
1 parent 551d9cb commit 61ff442
Show file tree
Hide file tree
Showing 38 changed files with 1,105 additions and 76 deletions.
11 changes: 11 additions & 0 deletions api/src/models/api/nova/IAddressDetailsRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface IAddressDetailsRequest {
/**
* The network to search on.
*/
network: string;

/**
* The bech32 address to get the basic output ids for.
*/
address: string;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/IAddressDetailsResponse.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 IAddressDetailsResponse extends IResponse {
/**
* The outputs data.
*/
outputs?: OutputResponse[];
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/foundry/IFoundriesRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface IFoundriesRequest {
/**
* The network to search on.
*/
network: string;

/**
* The bech32 account address to get the foundy output ids for.
*/
accountAddress: string;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/foundry/IFoundriesResponse.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 { IOutputsResponse } from "@iota/sdk-nova";
import { IResponse } from "../IResponse";

export interface IFoundriesResponse extends IResponse {
/**
* The output ids response.
*/
foundryOutputsResponse?: IOutputsResponse;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/foundry/IFoundryRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface IFoundryRequest {
/**
* The network to search on.
*/
network: string;

/**
* The foundry id to get the foundry details for.
*/
foundryId: string;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/foundry/IFoundryResponse.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 IFoundryResponse extends IResponse {
/**
* The foundry details response.
*/
foundryDetails?: OutputResponse;
}
13 changes: 13 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,26 @@ export const routes: IRoute[] = [
{ 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/foundry/:network/:foundryId", method: "get", folder: "nova/foundry", func: "get" },
{
path: "/nova/address/outputs/basic/:network/:address",
method: "get",
folder: "nova/address/outputs/basic",
func: "get",
},
{
path: "/nova/output/associated/:network/:address",
method: "post",
folder: "nova/output/associated",
func: "post",
dataBody: true,
},
{
path: "/nova/account/foundries/:network/:accountAddress",
method: "get",
folder: "nova/account/foundries",
func: "get",
},
{ path: "/nova/block/:network/:blockId", method: "get", folder: "nova/block", func: "get" },
{ path: "/nova/block/metadata/:network/:blockId", method: "get", folder: "nova/block/metadata", func: "get" },
];
30 changes: 30 additions & 0 deletions api/src/routes/nova/account/foundries/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServiceFactory } from "../../../../factories/serviceFactory";
import { IFoundriesRequest } from "../../../../models/api/nova/foundry/IFoundriesRequest";
import { IFoundriesResponse } from "../../../../models/api/nova/foundry/IFoundriesResponse";
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 controlled Foundry output id by controller Account address
* @param config The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(config: IConfiguration, request: IFoundriesRequest): Promise<IFoundriesResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.string(request.accountAddress, "accountAddress");

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

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const novaApiService = ServiceFactory.get<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.accountFoundries(request.accountAddress);
}
29 changes: 29 additions & 0 deletions api/src/routes/nova/address/outputs/basic/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ServiceFactory } from "../../../../../factories/serviceFactory";
import { IAddressDetailsRequest } from "../../../../../models/api/nova/IAddressDetailsRequest";
import { IAddressDetailsResponse } from "../../../../../models/api/nova/IAddressDetailsResponse";
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";

/**
* Fetch the basic output details by address.
* @param config The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(config: IConfiguration, request: IAddressDetailsRequest): Promise<IAddressDetailsResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");

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

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const novaApiService = ServiceFactory.get<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.basicOutputDetailsByAddress(request.address);
}
30 changes: 30 additions & 0 deletions api/src/routes/nova/foundry/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServiceFactory } from "../../../factories/serviceFactory";
import { IFoundryRequest } from "../../../models/api/nova/foundry/IFoundryRequest";
import { IFoundryResponse } from "../../../models/api/nova/foundry/IFoundryResponse";
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 foundry output details by Foundry id.
* @param config The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(config: IConfiguration, request: IFoundryRequest): Promise<IFoundryResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.string(request.foundryId, "foundryId");

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

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const novaApiService = ServiceFactory.get<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.foundryDetails(request.foundryId);
}
99 changes: 98 additions & 1 deletion api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Client } from "@iota/sdk-nova";
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { Client, OutputResponse } from "@iota/sdk-nova";
import { ServiceFactory } from "../../factories/serviceFactory";
import logger from "../../logger";
import { IFoundriesResponse } from "../../models/api/nova/foundry/IFoundriesResponse";
import { IFoundryResponse } from "../../models/api/nova/foundry/IFoundryResponse";
import { IAccountDetailsResponse } from "../../models/api/nova/IAccountDetailsResponse";
import { IAddressDetailsResponse } from "../../models/api/nova/IAddressDetailsResponse";
import { IAnchorDetailsResponse } from "../../models/api/nova/IAnchorDetailsResponse";
import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse";
import { IBlockResponse } from "../../models/api/nova/IBlockResponse";
Expand Down Expand Up @@ -153,6 +157,99 @@ export class NovaApiService {
}
}

/**
* Get controlled Foundry output id by controller Account address
* @param accountAddress The bech32 account address to get the controlled Foundries for.
* @returns The foundry outputs.
*/
public async accountFoundries(accountAddress: string): Promise<IFoundriesResponse | undefined> {
try {
const response = await this.client.foundryOutputIds({ account: accountAddress });

if (response) {
return {
foundryOutputsResponse: response,
};
}
} catch {
return { message: "Foundries output not found" };
}
}

/**
* Get the foundry details.
* @param foundryId The foundryId to get the details for.
* @returns The foundry details.
*/
public async foundryDetails(foundryId: string): Promise<IFoundryResponse | undefined> {
try {
const foundryOutputId = await this.client.foundryOutputId(foundryId);

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

return outputResponse.error ? { error: outputResponse.error } : { foundryDetails: outputResponse.output };
}
} catch {
return { message: "Foundry output not found" };
}
}

/**
* Get the outputs details.
* @param outputIds The output ids to get the details.
* @returns The item details.
*/
public async outputsDetails(outputIds: string[]): Promise<OutputResponse[]> {
const promises: Promise<IOutputDetailsResponse>[] = [];
const outputResponses: OutputResponse[] = [];

for (const outputId of outputIds) {
const promise = this.outputDetails(outputId);
promises.push(promise);
}
try {
await Promise.all(promises).then((results) => {
for (const outputDetails of results) {
if (outputDetails.output?.output && outputDetails.output?.metadata) {
outputResponses.push(outputDetails.output);
}
}
});

return outputResponses;
} catch (e) {
logger.error(`Fetching outputs details failed. Cause: ${e}`);
}
}

/**
* Get the relevant basic output details for an address.
* @param addressBech32 The address in bech32 format.
* @returns The basic output details.
*/
public async basicOutputDetailsByAddress(addressBech32: string): Promise<IAddressDetailsResponse> {
let cursor: string | undefined;
let outputIds: string[] = [];

do {
try {
const outputIdsResponse = await this.client.basicOutputIds({ address: addressBech32, cursor: cursor ?? "" });

outputIds = outputIds.concat(outputIdsResponse.items);
cursor = outputIdsResponse.cursor;
} catch (e) {
logger.error(`Fetching basic output ids failed. Cause: ${e}`);
}
} while (cursor);

const outputResponses = await this.outputsDetails(outputIds);

return {
outputs: outputResponses,
};
}

/**
* Get the output mana rewards.
* @param outputId The outputId to get the rewards for.
Expand Down
14 changes: 7 additions & 7 deletions client/src/app/components/nova/address/AccountAddressView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AccountAddress } from "@iota/sdk-wasm-nova/web";
import React from "react";
import { useAccountAddressState } from "~/helpers/nova/hooks/useAccountAddressState";
import Spinner from "../../Spinner";
import Spinner from "~/app/components/Spinner";
import Bech32Address from "../../nova/address/Bech32Address";
import { AddressPageTabbedSections } from "./section/AddressPageTabbedSections";
import AddressBalance from "./AddressBalance";
Expand All @@ -12,17 +12,17 @@ interface AccountAddressViewProps {

const AccountAddressView: React.FC<AccountAddressViewProps> = ({ accountAddress }) => {
const [state, setState] = useAccountAddressState(accountAddress);
const { accountAddressDetails, totalBalance, availableBalance, isAccountDetailsLoading, isAssociatedOutputsLoading } = state;
const { addressDetails, totalBalance, availableBalance, isAccountDetailsLoading, isAssociatedOutputsLoading } = state;
const isPageLoading = isAccountDetailsLoading || isAssociatedOutputsLoading;

return (
<div className="address-page">
<div className="wrapper">
{accountAddressDetails && (
{addressDetails && (
<div className="inner">
<div className="addr--header">
<div className="row middle">
<h1>{accountAddressDetails.label?.replace("Ed25519", "Address")}</h1>
<h1>{addressDetails.label?.replace("Ed25519", "Address")}</h1>
</div>
{isPageLoading && <Spinner />}
</div>
Expand All @@ -34,7 +34,7 @@ const AccountAddressView: React.FC<AccountAddressViewProps> = ({ accountAddress
</div>
<div className="general-content">
<div className="section--data">
<Bech32Address addressDetails={accountAddressDetails} advancedMode={true} />
<Bech32Address addressDetails={addressDetails} advancedMode={true} />
{totalBalance !== null && (
<AddressBalance
totalBalance={totalBalance}
Expand All @@ -46,8 +46,8 @@ const AccountAddressView: React.FC<AccountAddressViewProps> = ({ accountAddress
</div>
</div>
<AddressPageTabbedSections
key={accountAddressDetails.bech32}
addressDetails={accountAddressDetails}
key={addressDetails.bech32}
addressState={state}
setAssociatedOutputsLoading={(val) => setState({ isAssociatedOutputsLoading: val })}
/>
</div>
Expand Down
Loading

0 comments on commit 61ff442

Please sign in to comment.