diff --git a/.github/workflows/nova-build-temp.yaml b/.github/workflows/nova-build-temp.yaml index 7b718a706..ed00fb84f 100644 --- a/.github/workflows/nova-build-temp.yaml +++ b/.github/workflows/nova-build-temp.yaml @@ -6,7 +6,7 @@ on: TARGET_COMMIT: description: "Target Commit Hash for the SDK" required: false - default: "fc9f0f56bb5cfc146993e53aa9656ded220734e1" + default: "aa1b1de58731dbbf9dd0f5e2960fd11b0056b633" environment: type: choice description: "Select the environment to deploy to" diff --git a/api/src/models/api/nova/IAccountValidatorDetailsRequest.ts b/api/src/models/api/nova/IAccountValidatorDetailsRequest.ts new file mode 100644 index 000000000..0e4b099da --- /dev/null +++ b/api/src/models/api/nova/IAccountValidatorDetailsRequest.ts @@ -0,0 +1,11 @@ +export interface IAccountValidatorDetailsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The account id to get the validator details for. + */ + accountId: string; +} diff --git a/api/src/models/api/nova/IAccountValidatorDetailsResponse.ts b/api/src/models/api/nova/IAccountValidatorDetailsResponse.ts new file mode 100644 index 000000000..13b205e30 --- /dev/null +++ b/api/src/models/api/nova/IAccountValidatorDetailsResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { ValidatorResponse } from "@iota/sdk-nova"; +import { IResponse } from "./IResponse"; + +export interface IAccountValidatorDetailsResponse extends IResponse { + /** + * The account validator response. + */ + validatorDetails?: ValidatorResponse; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index f13155368..776b6cecc 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -260,6 +260,12 @@ export const routes: IRoute[] = [ folder: "nova/account/congestion", func: "get", }, + { + path: "/nova/account/validator/:network/:accountId", + method: "get", + folder: "nova/account/validator", + 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" }, { path: "/nova/slot/:network/:slotIndex", method: "get", folder: "nova/slot", func: "get" }, diff --git a/api/src/routes/nova/account/validator/get.ts b/api/src/routes/nova/account/validator/get.ts new file mode 100644 index 000000000..f4e837c4f --- /dev/null +++ b/api/src/routes/nova/account/validator/get.ts @@ -0,0 +1,30 @@ +import { ServiceFactory } from "../../../../factories/serviceFactory"; +import { IAccountValidatorDetailsRequest } from "../../../../models/api/nova/IAccountValidatorDetailsRequest"; +import { IAccountValidatorDetailsResponse } from "../../../../models/api/nova/IAccountValidatorDetailsResponse"; +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 validator details for Account address + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: IAccountValidatorDetailsRequest): 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.getAccountValidatorDetails(request.accountId); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index ddd3516c1..0fc9b4456 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -7,6 +7,7 @@ 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 { IAccountValidatorDetailsResponse } from "../../models/api/nova/IAccountValidatorDetailsResponse"; import { IAddressDetailsResponse } from "../../models/api/nova/IAddressDetailsResponse"; import { IAnchorDetailsResponse } from "../../models/api/nova/IAnchorDetailsResponse"; import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse"; @@ -322,6 +323,25 @@ export class NovaApiService { } } + /** + * Get validator details for Account + * @param accountId The account id to get the validator details for. + * @returns The Congestion. + */ + public async getAccountValidatorDetails(accountId: string): Promise { + try { + const response = await this.client.getValidator(accountId); + + if (response) { + return { + validatorDetails: response, + }; + } + } catch { + return { message: "Validator details 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 59910d959..38b39c2f0 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -3,6 +3,7 @@ import associatedOuputsMessage from "~assets/modals/stardust/address/associated- import foundriesMessage from "~assets/modals/stardust/alias/foundries.json"; import stateMessage from "~assets/modals/stardust/alias/state.json"; import bicMessage from "~assets/modals/nova/account/bic.json"; +import validatorMessage from "~assets/modals/nova/account/validator.json"; import nftMetadataMessage from "~assets/modals/stardust/nft/metadata.json"; import addressNftsMessage from "~assets/modals/stardust/address/nfts-in-wallet.json"; import TabbedSection from "../../../hoc/TabbedSection"; @@ -21,6 +22,7 @@ import AnchorStateSection from "./anchor/AnchorStateSection"; import NftSection from "~/app/components/nova/address/section/nft/NftSection"; import NftMetadataSection from "~/app/components/nova/address/section/nft/NftMetadataSection"; import { TransactionsHelper } from "~/helpers/nova/transactionsHelper"; +import AccountValidatorSection from "./account/AccountValidatorSection"; enum DEFAULT_TABS { AssocOutputs = "Outputs", @@ -31,6 +33,7 @@ enum DEFAULT_TABS { enum ACCOUNT_TABS { BlockIssuance = "Block Issuance", Foundries = "Foundries", + Validation = "Validation", } enum ANCHOR_TABS { @@ -74,7 +77,9 @@ const buildAccountAddressTabsOptions = ( isBlockIssuer: boolean, isCongestionLoading: boolean, foundriesCount: number, + hasStakingFeature: boolean, isAccountFoundriesLoading: boolean, + isValidatorDetailsLoading: boolean, ) => ({ [ACCOUNT_TABS.Foundries]: { disabled: foundriesCount === 0, @@ -88,6 +93,12 @@ const buildAccountAddressTabsOptions = ( isLoading: isCongestionLoading, infoContent: bicMessage, }, + [ACCOUNT_TABS.Validation]: { + disabled: !hasStakingFeature, + hidden: !hasStakingFeature, + isLoading: isValidatorDetailsLoading, + infoContent: validatorMessage, + }, }); const buildAnchorAddressTabsOptions = (isAnchorStateTabDisabled: boolean, isAnchorDetailsLoading: boolean) => ({ @@ -151,6 +162,10 @@ export const AddressPageTabbedSections: React.FC, + , ] : null; @@ -186,7 +201,9 @@ export const AddressPageTabbedSections: React.FC = ({ validatorDetails }) => { + if (!validatorDetails) { + return null; + } + + const delegatedStake = BigInt(validatorDetails.poolStake) - BigInt(validatorDetails.validatorStake); + + return ( +
+
+
+
Active
+
{validatorDetails.active ? "Yes" : "No"}
+
+
+
Staking End Epoch
+
{validatorDetails.stakingEndEpoch}
+
+
+
Pool Stake
+
{String(validatorDetails.poolStake)}
+
+
+
Validator Stake
+
{String(validatorDetails.validatorStake)}
+
+
+
Delegated Stake
+
{String(delegatedStake)}
+
+
+
Fixed Cost
+
{Number(validatorDetails?.fixedCost)}
+
+
+
Latest Supported Protocol Version
+
{validatorDetails?.latestSupportedProtocolVersion}
+
+
+
Latest Supported Protocol Hash
+
{validatorDetails?.latestSupportedProtocolHash}
+
+
+
+ ); +}; + +export default AccountValidatorSection; diff --git a/client/src/assets/modals/nova/account/validator.json b/client/src/assets/modals/nova/account/validator.json new file mode 100644 index 000000000..a512acaed --- /dev/null +++ b/client/src/assets/modals/nova/account/validator.json @@ -0,0 +1,11 @@ +{ + "title": "Validator", + "description": "

Validators are special nodes that issue validation blocks.

", + "links": [ + { + "label": "Read more", + "href": "https://wiki.iota.org/learn/protocols/iota2.0/core-concepts/validators", + "isExternal": true + } + ] +} diff --git a/client/src/helpers/nova/hooks/useAccountAddressState.ts b/client/src/helpers/nova/hooks/useAccountAddressState.ts index 2a1438b63..d25c56bab 100644 --- a/client/src/helpers/nova/hooks/useAccountAddressState.ts +++ b/client/src/helpers/nova/hooks/useAccountAddressState.ts @@ -6,6 +6,8 @@ import { CongestionResponse, FeatureType, OutputResponse, + StakingFeature, + ValidatorResponse, } from "@iota/sdk-wasm-nova/web"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; import { useAccountDetails } from "./useAccountDetails"; @@ -18,6 +20,7 @@ import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutp import { useAccountControlledFoundries } from "./useAccountControlledFoundries"; import { useAccountCongestion } from "./useAccountCongestion"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; +import { useAccountValidatorDetails } from "./useAccountValidatorDetails"; export interface IAccountAddressState { addressDetails: IAddressDetails | null; @@ -25,6 +28,8 @@ export interface IAccountAddressState { totalBalance: number | null; availableBalance: number | null; blockIssuerFeature: BlockIssuerFeature | null; + stakingFeature: StakingFeature | null; + validatorDetails: ValidatorResponse | null; addressBasicOutputs: OutputResponse[] | null; addressNftOutputs: OutputResponse[] | null; foundries: string[] | null; @@ -35,6 +40,7 @@ export interface IAccountAddressState { isNftOutputsLoading: boolean; isFoundriesLoading: boolean; isCongestionLoading: boolean; + isValidatorDetailsLoading: boolean; } const initialState = { @@ -43,6 +49,8 @@ const initialState = { totalBalance: null, availableBalance: null, blockIssuerFeature: null, + stakingFeature: null, + validatorDetails: null, addressBasicOutputs: null, addressNftOutputs: null, foundries: null, @@ -53,6 +61,7 @@ const initialState = { isNftOutputsLoading: false, isFoundriesLoading: false, isCongestionLoading: false, + isValidatorDetailsLoading: false, }; /** @@ -72,11 +81,16 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres ); const { accountOutput, isLoading: isAccountDetailsLoading } = useAccountDetails(network, address.accountId); + const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, accountOutput); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); const [foundries, isFoundriesLoading] = useAccountControlledFoundries(network, state.addressDetails); const { congestion, isLoading: isCongestionLoading } = useAccountCongestion(network, state.addressDetails?.hex ?? null); + const { validatorDetails, isLoading: isValidatorDetailsLoading } = useAccountValidatorDetails( + network, + state.addressDetails?.hex ?? null, + ); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -98,23 +112,36 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres availableBalance, foundries, congestion, + validatorDetails, addressBasicOutputs, addressNftOutputs, isBasicOutputsLoading, isNftOutputsLoading, isFoundriesLoading, isCongestionLoading, + isValidatorDetailsLoading, }; - if (accountOutput && !state.blockIssuerFeature) { - const blockIssuerFeature = accountOutput?.features?.find( - (feature) => feature.type === FeatureType.BlockIssuer, - ) as BlockIssuerFeature; - if (blockIssuerFeature) { - updatedState = { - ...updatedState, - blockIssuerFeature, - }; + if (accountOutput) { + if (!state.blockIssuerFeature) { + const blockIssuerFeature = accountOutput?.features?.find( + (feature) => feature.type === FeatureType.BlockIssuer, + ) as BlockIssuerFeature; + if (blockIssuerFeature) { + updatedState = { + ...updatedState, + blockIssuerFeature, + }; + } + } + if (!state.stakingFeature) { + const stakingFeature = accountOutput?.features?.find((feature) => feature.type === FeatureType.Staking) as StakingFeature; + if (stakingFeature) { + updatedState = { + ...updatedState, + stakingFeature, + }; + } } } @@ -126,10 +153,12 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres addressBasicOutputs, addressNftOutputs, congestion, + validatorDetails, isAccountDetailsLoading, isBasicOutputsLoading, isNftOutputsLoading, isCongestionLoading, + isValidatorDetailsLoading, ]); return [state, setState]; diff --git a/client/src/helpers/nova/hooks/useAccountValidatorDetails.ts b/client/src/helpers/nova/hooks/useAccountValidatorDetails.ts new file mode 100644 index 000000000..62c0450df --- /dev/null +++ b/client/src/helpers/nova/hooks/useAccountValidatorDetails.ts @@ -0,0 +1,48 @@ +import { ValidatorResponse } 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 validator information + * @param network The Network in context + * @param accountId The account id + * @returns The output response and loading bool. + */ +export function useAccountValidatorDetails( + network: string, + accountId: string | null, +): { validatorDetails: ValidatorResponse | null; isLoading: boolean } { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [validatorDetails, setValidatorDetails] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (accountId) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .getAccountValidatorDetails({ + network, + accountId, + }) + .then((response) => { + if (!response?.error && isMounted) { + setValidatorDetails(response.validatorDetails ?? null); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, accountId]); + + return { validatorDetails, isLoading }; +} diff --git a/client/src/models/api/nova/IAccountValidatorDetailsRequest.ts b/client/src/models/api/nova/IAccountValidatorDetailsRequest.ts new file mode 100644 index 000000000..0e4b099da --- /dev/null +++ b/client/src/models/api/nova/IAccountValidatorDetailsRequest.ts @@ -0,0 +1,11 @@ +export interface IAccountValidatorDetailsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The account id to get the validator details for. + */ + accountId: string; +} diff --git a/client/src/models/api/nova/IAccountValidatorDetailsResponse.ts b/client/src/models/api/nova/IAccountValidatorDetailsResponse.ts new file mode 100644 index 000000000..8a41fba3f --- /dev/null +++ b/client/src/models/api/nova/IAccountValidatorDetailsResponse.ts @@ -0,0 +1,9 @@ +import { ValidatorResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "./IResponse"; + +export interface IAccountValidatorDetailsResponse extends IResponse { + /** + * The account validator response. + */ + validatorDetails?: ValidatorResponse; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index a441e0722..81152f01f 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -35,6 +35,8 @@ import { ITransactionDetailsRequest } from "~/models/api/nova/ITransactionDetail import { ITransactionDetailsResponse } from "~/models/api/nova/ITransactionDetailsResponse"; import { ICongestionRequest } from "~/models/api/nova/ICongestionRequest"; import { ICongestionResponse } from "~/models/api/nova/ICongestionResponse"; +import { IAccountValidatorDetailsRequest } from "~/models/api/nova/IAccountValidatorDetailsRequest"; +import { IAccountValidatorDetailsResponse } from "~/models/api/nova/IAccountValidatorDetailsResponse"; /** * Class to handle api communications on nova. @@ -179,6 +181,15 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/account/congestion/${request.network}/${request.accountId}`, "get"); } + /** + * Get the account validator info. + * @param request The request to send. + * @returns The response from the request. + */ + public async getAccountValidatorDetails(request: IAccountValidatorDetailsRequest): Promise { + return this.callApi(`nova/account/validator/${request.network}/${request.accountId}`, "get"); + } + /** * Get the output mana rewards. * @param request The request to send. diff --git a/setup_nova.sh b/setup_nova.sh index b69fcd007..adc14bd82 100755 --- a/setup_nova.sh +++ b/setup_nova.sh @@ -1,6 +1,6 @@ #!/bin/bash SDK_DIR="iota-sdk" -TARGET_COMMIT="fc9f0f56bb5cfc146993e53aa9656ded220734e1" +TARGET_COMMIT="aa1b1de58731dbbf9dd0f5e2960fd11b0056b633" if [ ! -d "$SDK_DIR" ]; then git clone -b 2.0 git@github.com:iotaledger/iota-sdk.git