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: Add Native tokens Tab to address pages #1119

Merged
merged 21 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a528704
feat: add output tab to address page
brancoder Feb 13, 2024
3e24988
Merge branch 'dev' into feat/add-outputs-tab
brancoder Feb 13, 2024
aed2313
fix: add components to display native tokens
brancoder Feb 14, 2024
cd0e4ed
fix: resolve conflicts
brancoder Feb 14, 2024
8ffe165
Remove unused code in AddressPageTabbedSections component
brancoder Feb 14, 2024
a55715b
fix: resolve conflicts
brancoder Feb 14, 2024
4c9cf07
fix: resolve confilict
brancoder Feb 14, 2024
44fd78a
remove unused interfaces
brancoder Feb 14, 2024
35d1cbb
Update address states and Add basic outputs API endpoint
brancoder Feb 15, 2024
99df7c0
Merge branch 'dev' into feat/add-native-tokens-tab
brancoder Feb 15, 2024
3a10fe5
Add eslint-disable for unsafe return in novaApiService.ts
brancoder Feb 15, 2024
b390f05
Add Foundries tab to Accound address page (#1134)
brancoder Feb 16, 2024
0f0b1c5
fix: validation check in foundries endpoint
brancoder Feb 16, 2024
9db3390
fix: component imports
brancoder Feb 16, 2024
1b665f1
Merge branch 'dev' into feat/add-native-tokens-tab
brancoder Feb 16, 2024
f2e9aea
fix: imports
brancoder Feb 16, 2024
705c9eb
Merge branch 'dev' into feat/add-native-tokens-tab
msarcev Feb 16, 2024
2f678bb
Merge branch 'dev' into feat/add-native-tokens-tab
msarcev Feb 16, 2024
34da753
Merge branch 'dev' into feat/add-native-tokens-tab
msarcev Feb 16, 2024
babaf50
fix: select first tab on address page
brancoder Feb 18, 2024
b1322a3
Merge branch 'feat/add-native-tokens-tab' of github.com:iotaledger/ex…
brancoder Feb 18, 2024
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
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
Loading