From 42477e33b1a8ab3ff508e39534dcb8a30d60b0fb Mon Sep 17 00:00:00 2001 From: Bran <52735957+brancoder@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:41:53 +0100 Subject: [PATCH] Feat: Add Block issuance tab to Account Address (#1142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Add Foundries tab to Accound address page (#1134) * feat: add block issuance tab to account page * fix: validation check in foundries endpoint * fix: component imports * fix: add is congestion loading * Update paths in tsconfig.json --------- Co-authored-by: Begoña Álvarez de la Cruz --- api/src/models/api/nova/ICongestionRequest.ts | 11 +++++ .../models/api/nova/ICongestionResponse.ts | 11 +++++ api/src/routes.ts | 6 +++ api/src/routes/nova/account/congestion/get.ts | 30 ++++++++++++ api/src/services/nova/novaApiService.ts | 20 ++++++++ .../section/AddressPageTabbedSections.tsx | 28 +++++++++-- .../account/AccountBlockIssuanceSection.scss | 14 ++++++ .../account/AccountBlockIssuanceSection.tsx | 46 ++++++++++++++++++ .../src/assets/modals/nova/account/bic.json | 11 +++++ .../nova/hooks/useAccountAddressState.ts | 48 +++++++++++++++++-- .../nova/hooks/useAccountCongestion.ts | 48 +++++++++++++++++++ .../src/models/api/nova/ICongestionRequest.ts | 11 +++++ .../models/api/nova/ICongestionResponse.ts | 11 +++++ client/src/services/nova/novaApiClient.ts | 11 +++++ 14 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 api/src/models/api/nova/ICongestionRequest.ts create mode 100644 api/src/models/api/nova/ICongestionResponse.ts create mode 100644 api/src/routes/nova/account/congestion/get.ts create mode 100644 client/src/app/components/nova/address/section/account/AccountBlockIssuanceSection.scss create mode 100644 client/src/app/components/nova/address/section/account/AccountBlockIssuanceSection.tsx create mode 100644 client/src/assets/modals/nova/account/bic.json create mode 100644 client/src/helpers/nova/hooks/useAccountCongestion.ts create mode 100644 client/src/models/api/nova/ICongestionRequest.ts create mode 100644 client/src/models/api/nova/ICongestionResponse.ts diff --git a/api/src/models/api/nova/ICongestionRequest.ts b/api/src/models/api/nova/ICongestionRequest.ts new file mode 100644 index 000000000..00e845db6 --- /dev/null +++ b/api/src/models/api/nova/ICongestionRequest.ts @@ -0,0 +1,11 @@ +export interface ICongestionRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The account id to get the congestion for. + */ + accountId: string; +} diff --git a/api/src/models/api/nova/ICongestionResponse.ts b/api/src/models/api/nova/ICongestionResponse.ts new file mode 100644 index 000000000..9e7fff964 --- /dev/null +++ b/api/src/models/api/nova/ICongestionResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { CongestionResponse } from "@iota/sdk-nova"; +import { IResponse } from "./IResponse"; + +export interface ICongestionResponse extends IResponse { + /** + * The Account Congestion. + */ + congestion?: CongestionResponse; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index 4fbceb165..b5051dbc4 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -242,6 +242,12 @@ export const routes: IRoute[] = [ folder: "nova/account/foundries", func: "get", }, + { + path: "/nova/account/congestion/:network/:accountId", + method: "get", + folder: "nova/account/congestion", + 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" }, ]; diff --git a/api/src/routes/nova/account/congestion/get.ts b/api/src/routes/nova/account/congestion/get.ts new file mode 100644 index 000000000..4ab680766 --- /dev/null +++ b/api/src/routes/nova/account/congestion/get.ts @@ -0,0 +1,30 @@ +import { ServiceFactory } from "../../../../factories/serviceFactory"; +import { ICongestionRequest } from "../../../../models/api/nova/ICongestionRequest"; +import { ICongestionResponse } from "../../../../models/api/nova/ICongestionResponse"; +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 Congestion for Account address + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: ICongestionRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.accountId, "accountId"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.getAccountCongestion(request.accountId); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 09d70ff27..48d25f8fe 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -11,6 +11,7 @@ import { IAddressDetailsResponse } from "../../models/api/nova/IAddressDetailsRe import { IAnchorDetailsResponse } from "../../models/api/nova/IAnchorDetailsResponse"; import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse"; import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; +import { ICongestionResponse } from "../../models/api/nova/ICongestionResponse"; import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse"; @@ -250,6 +251,25 @@ export class NovaApiService { }; } + /** + * Get Congestion for Account + * @param accountId The account address to get the congestion for. + * @returns The Congestion. + */ + public async getAccountCongestion(accountId: string): Promise { + try { + const response = await this.client.getAccountCongestion(accountId); + + if (response) { + return { + congestion: response, + }; + } + } catch { + return { message: "Account congestion not found" }; + } + } + /** * Get the output mana rewards. * @param outputId The outputId to get the rewards for. diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx index 29410a48d..d4b979180 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import associatedOuputsMessage from "~assets/modals/stardust/address/associated-outputs.json"; import foundriesMessage from "~assets/modals/stardust/alias/foundries.json"; +import bicMessage from "~assets/modals/nova/account/bic.json"; import TabbedSection from "../../../hoc/TabbedSection"; import AssociatedOutputs from "./association/AssociatedOutputs"; import nativeTokensMessage from "~assets/modals/stardust/address/assets-in-wallet.json"; @@ -12,6 +13,7 @@ import AssetsTable from "./native-tokens/AssetsTable"; import { IImplicitAccountCreationAddressState } from "~/helpers/nova/hooks/useImplicitAccountCreationAddressState"; import { AddressType } from "@iota/sdk-wasm-nova/web"; import AccountFoundriesSection from "./account/AccountFoundriesSection"; +import AccountBlockIssuanceSection from "./account/AccountBlockIssuanceSection"; enum DEFAULT_TABS { AssocOutputs = "Outputs", @@ -19,6 +21,7 @@ enum DEFAULT_TABS { } enum ACCOUNT_TABS { + BlockIssuance = "Block Issuance", Foundries = "Foundries", } @@ -37,13 +40,24 @@ const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: num }, }); -const buildAccountAddressTabsOptions = (foundriesCount: number, isAccountFoundriesLoading: boolean) => ({ +const buildAccountAddressTabsOptions = ( + isBlockIssuer: boolean, + isCongestionLoading: boolean, + foundriesCount: number, + isAccountFoundriesLoading: boolean, +) => ({ [ACCOUNT_TABS.Foundries]: { disabled: foundriesCount === 0, hidden: foundriesCount === 0, isLoading: isAccountFoundriesLoading, infoContent: foundriesMessage, }, + [ACCOUNT_TABS.BlockIssuance]: { + disabled: !isBlockIssuer, + hidden: !isBlockIssuer, + isLoading: isCongestionLoading, + infoContent: bicMessage, + }, }); interface IAddressPageTabbedSectionsProps { @@ -78,6 +92,11 @@ export const AddressPageTabbedSections: React.FC, = ({ blockIssuerFeature, congestion }) => { + return ( +
+
+
+
Current Slot
+
{congestion?.slot}
+
+
+
Block Issuance Credit
+
{congestion?.blockIssuanceCredits.toString()}
+
+
+
Referenced Mana Cost
+
{congestion?.referenceManaCost.toString()}
+
+
+
Expiry Slot
+
{blockIssuerFeature?.expirySlot}
+
+
+ {blockIssuerFeature?.blockIssuerKeys.map((key) => ( + + Public Key: +
+ +
+
+ ))} +
+
+
+ ); +}; + +export default AccountBlockIssuanceSection; diff --git a/client/src/assets/modals/nova/account/bic.json b/client/src/assets/modals/nova/account/bic.json new file mode 100644 index 000000000..3b875279d --- /dev/null +++ b/client/src/assets/modals/nova/account/bic.json @@ -0,0 +1,11 @@ +{ + "title": "Block Issuance Credit", + "description": "

(BIC) is the form of Mana used as an anti-spam mechanism to the block issuance process.

", + "links": [ + { + "label": "Read more", + "href": "https://wiki.iota.org/learn/protocols/iota2.0/core-concepts/mana/#block-issuance-credits-bic", + "isExternal": true + } + ] +} diff --git a/client/src/helpers/nova/hooks/useAccountAddressState.ts b/client/src/helpers/nova/hooks/useAccountAddressState.ts index a92f679db..09c5ec62d 100644 --- a/client/src/helpers/nova/hooks/useAccountAddressState.ts +++ b/client/src/helpers/nova/hooks/useAccountAddressState.ts @@ -1,5 +1,12 @@ import { Reducer, useEffect, useReducer } from "react"; -import { AccountAddress, AccountOutput, OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { + AccountAddress, + AccountOutput, + BlockIssuerFeature, + CongestionResponse, + FeatureType, + OutputResponse, +} from "@iota/sdk-wasm-nova/web"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; import { useAccountDetails } from "./useAccountDetails"; import { useLocation, useParams } from "react-router-dom"; @@ -9,18 +16,22 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAccountControlledFoundries } from "./useAccountControlledFoundries"; +import { useAccountCongestion } from "./useAccountCongestion"; export interface IAccountAddressState { addressDetails: IAddressDetails | null; accountOutput: AccountOutput | null; totalBalance: number | null; availableBalance: number | null; + blockIssuerFeature: BlockIssuerFeature | null; addressBasicOutputs: OutputResponse[] | null; foundries: string[] | null; + congestion: CongestionResponse | null; isAccountDetailsLoading: boolean; isAssociatedOutputsLoading: boolean; isBasicOutputsLoading: boolean; isFoundriesLoading: boolean; + isCongestionLoading: boolean; } const initialState = { @@ -28,12 +39,15 @@ const initialState = { accountOutput: null, totalBalance: null, availableBalance: null, + blockIssuerFeature: null, addressBasicOutputs: null, foundries: null, + congestion: null, isAccountDetailsLoading: true, isAssociatedOutputsLoading: false, isBasicOutputsLoading: false, isFoundriesLoading: false, + isCongestionLoading: false, }; /** @@ -56,6 +70,7 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, accountOutput); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [foundries, isFoundriesLoading] = useAccountControlledFoundries(network, state.addressDetails); + const { congestion, isLoading: isCongestionLoading } = useAccountCongestion(network, state.addressDetails?.hex ?? null); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -70,17 +85,42 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres }, []); useEffect(() => { - setState({ + let updatedState: Partial = { accountOutput, isAccountDetailsLoading, totalBalance, availableBalance, foundries, + congestion, addressBasicOutputs, isBasicOutputsLoading, isFoundriesLoading, - }); - }, [accountOutput, totalBalance, availableBalance, addressBasicOutputs, isAccountDetailsLoading, isBasicOutputsLoading]); + isCongestionLoading, + }; + + if (accountOutput && !state.blockIssuerFeature) { + const blockIssuerFeature = accountOutput?.features?.find( + (feature) => feature.type === FeatureType.BlockIssuer, + ) as BlockIssuerFeature; + if (blockIssuerFeature) { + updatedState = { + ...updatedState, + blockIssuerFeature, + }; + } + } + + setState(updatedState); + }, [ + accountOutput, + totalBalance, + availableBalance, + addressBasicOutputs, + congestion, + isAccountDetailsLoading, + isBasicOutputsLoading, + isCongestionLoading, + ]); return [state, setState]; }; diff --git a/client/src/helpers/nova/hooks/useAccountCongestion.ts b/client/src/helpers/nova/hooks/useAccountCongestion.ts new file mode 100644 index 000000000..7ca8eafd9 --- /dev/null +++ b/client/src/helpers/nova/hooks/useAccountCongestion.ts @@ -0,0 +1,48 @@ +import { CongestionResponse } from "@iota/sdk-wasm-nova/web"; +import { useEffect, useState } from "react"; +import { ServiceFactory } from "~/factories/serviceFactory"; +import { useIsMounted } from "~/helpers/hooks/useIsMounted"; +import { NOVA } from "~/models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; + +/** + * Fetch account congestion + * @param network The Network in context + * @param accountId The account id + * @returns The output response and loading bool. + */ +export function useAccountCongestion( + network: string, + accountId: string | null, +): { congestion: CongestionResponse | null; isLoading: boolean } { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [congestion, setAccountCongestion] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (accountId) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .getAccountCongestion({ + network, + accountId, + }) + .then((response) => { + if (!response?.error && isMounted) { + setAccountCongestion(response.congestion ?? null); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, accountId]); + + return { congestion, isLoading }; +} diff --git a/client/src/models/api/nova/ICongestionRequest.ts b/client/src/models/api/nova/ICongestionRequest.ts new file mode 100644 index 000000000..00e845db6 --- /dev/null +++ b/client/src/models/api/nova/ICongestionRequest.ts @@ -0,0 +1,11 @@ +export interface ICongestionRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The account id to get the congestion for. + */ + accountId: string; +} diff --git a/client/src/models/api/nova/ICongestionResponse.ts b/client/src/models/api/nova/ICongestionResponse.ts new file mode 100644 index 000000000..cb176a6f3 --- /dev/null +++ b/client/src/models/api/nova/ICongestionResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { CongestionResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "./IResponse"; + +export interface ICongestionResponse extends IResponse { + /** + * The Account Congestion. + */ + congestion?: CongestionResponse; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index 0ad378576..859be2fcc 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -29,6 +29,8 @@ import { IAddressDetailsRequest } from "~/models/api/nova/address/IAddressDetail import { IAddressDetailsResponse } from "~/models/api/nova/address/IAddressDetailsResponse"; import { IFoundriesResponse } from "~/models/api/nova/foundry/IFoundriesResponse"; import { IFoundriesRequest } from "~/models/api/nova/foundry/IFoundriesRequest"; +import { ICongestionRequest } from "~/models/api/nova/ICongestionRequest"; +import { ICongestionResponse } from "~/models/api/nova/ICongestionResponse"; /** * Class to handle api communications on nova. @@ -146,6 +148,15 @@ export class NovaApiClient extends ApiClient { ); } + /** + * Get the account congestion. + * @param request The request to send. + * @returns The response from the request. + */ + public async getAccountCongestion(request: ICongestionRequest): Promise { + return this.callApi(`nova/account/congestion/${request.network}/${request.accountId}`, "get"); + } + /** * Get the output mana rewards. * @param request The request to send.