diff --git a/api/src/models/api/nova/IDelegationDetailsResponse.ts b/api/src/models/api/nova/IDelegationDetailsResponse.ts new file mode 100644 index 000000000..afadf2150 --- /dev/null +++ b/api/src/models/api/nova/IDelegationDetailsResponse.ts @@ -0,0 +1,9 @@ +import { IDelegationWithDetails } from "./IDelegationWithDetails"; +import { IResponse } from "./IResponse"; + +export interface IDelegationDetailsResponse extends IResponse { + /** + * The outputs data. + */ + outputs?: IDelegationWithDetails[]; +} diff --git a/api/src/models/api/nova/IDelegationWithDetails.ts b/api/src/models/api/nova/IDelegationWithDetails.ts new file mode 100644 index 000000000..bb419d816 --- /dev/null +++ b/api/src/models/api/nova/IDelegationWithDetails.ts @@ -0,0 +1,16 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { OutputWithMetadataResponse } from "@iota/sdk-nova"; +import { IRewardsResponse } from "./IRewardsResponse"; + +export interface IDelegationWithDetails { + /** + * The output. + */ + output: OutputWithMetadataResponse; + + /** + * The rewards for the output. + */ + rewards: IRewardsResponse; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index 3693b9686..522d45960 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -241,6 +241,12 @@ export const routes: IRoute[] = [ folder: "nova/address/outputs/nft", func: "get", }, + { + path: "/nova/address/outputs/delegation/:network/:address", + method: "get", + folder: "nova/address/outputs/delegation", + func: "get", + }, { path: "/nova/output/associated/:network/:address", method: "post", diff --git a/api/src/routes/nova/address/outputs/delegation/get.ts b/api/src/routes/nova/address/outputs/delegation/get.ts new file mode 100644 index 000000000..59d6fe9b8 --- /dev/null +++ b/api/src/routes/nova/address/outputs/delegation/get.ts @@ -0,0 +1,30 @@ +import { ServiceFactory } from "../../../../../factories/serviceFactory"; +import { IAddressDetailsRequest } from "../../../../../models/api/nova/IAddressDetailsRequest"; +import { IDelegationDetailsResponse } from "../../../../../models/api/nova/IDelegationDetailsResponse"; +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 delegation output details by address. + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: IAddressDetailsRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.address, "address"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.delegationOutputDetailsByAddress(request.address); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index db58c0969..f6b6bb591 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -13,6 +13,8 @@ import { IAnchorDetailsResponse } from "../../models/api/nova/IAnchorDetailsResp import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse"; import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; import { ICongestionResponse } from "../../models/api/nova/ICongestionResponse"; +import { IDelegationDetailsResponse } from "../../models/api/nova/IDelegationDetailsResponse"; +import { IDelegationWithDetails } from "../../models/api/nova/IDelegationWithDetails"; import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse"; @@ -270,6 +272,25 @@ export class NovaApiService { } } + /** + * Get the outputs mana rewards. + * @param outputIds The output ids to get the mana rewards for. + * @returns The rewards details. + */ + public async outputsRewardsDetails(outputIds: string[]): Promise { + const promises: Promise[] = []; + + for (const outputId of outputIds) { + const promise = this.getRewards(outputId); + promises.push(promise); + } + try { + return await Promise.all(promises); + } catch (e) { + logger.error(`Fetching outputs rewards failed. Cause: ${e}`); + } + } + /** * Get the relevant basic output details for an address. * @param addressBech32 The address in bech32 format. @@ -323,6 +344,40 @@ export class NovaApiService { }; } + /** + * Get the relevant basic output details for an address. + * @param addressBech32 The address in bech32 format. + * @returns The basic output details. + */ + public async delegationOutputDetailsByAddress(addressBech32: string): Promise { + let cursor: string | undefined; + let outputIds: string[] = []; + const delegationResponse: IDelegationWithDetails[] = []; + + do { + try { + const outputIdsResponse = await this.client.delegationOutputIds({ address: addressBech32, cursor: cursor ?? "" }); + + outputIds = outputIds.concat(outputIdsResponse.items); + cursor = outputIdsResponse.cursor; + } catch (e) { + logger.error(`Fetching delegation output ids failed. Cause: ${e}`); + } + } while (cursor); + + const outputRewards = await this.outputsRewardsDetails(outputIds); + const outputResponses = await this.outputsDetails(outputIds); + + for (const outputResponse of outputResponses) { + const matchingReward = outputRewards?.find((outputReward) => outputReward.outputId === outputResponse.metadata.outputId); + delegationResponse.push({ rewards: matchingReward, output: outputResponse }); + } + + return { + outputs: delegationResponse, + }; + } + /** * Get Congestion for Account * @param accountId The account address to get the congestion for. diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx index d3ae449e0..a70504c96 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -4,6 +4,7 @@ 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 delegationMessage from "~assets/modals/nova/delegation.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"; @@ -26,12 +27,14 @@ 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"; +import DelegationSection from "./delegation/DelegationSection"; enum DEFAULT_TABS { Transactions = "Transactions", AssocOutputs = "Outputs", NativeTokens = "Native Tokens", Nfts = "NFTs", + Delegation = "Delegation", } enum ACCOUNT_TABS { @@ -52,8 +55,10 @@ const buildDefaultTabsOptions = ( tokensCount: number, nftsCount: number, associatedOutputCount: number, + delegationCount: number, isNativeTokensLoading: boolean, isNftOutputsLoading: boolean, + isDelegationOutputsLoading: boolean, isAddressHistoryLoading: boolean, isAddressHistoryDisabled: boolean, ) => ({ @@ -83,6 +88,13 @@ const buildDefaultTabsOptions = ( isLoading: isNftOutputsLoading, infoContent: addressNftsMessage, }, + [DEFAULT_TABS.Delegation]: { + disabled: delegationCount === 0, + hidden: delegationCount === 0, + counter: delegationCount, + isLoading: isDelegationOutputsLoading, + infoContent: delegationMessage, + }, }); const buildAccountAddressTabsOptions = ( @@ -156,7 +168,15 @@ export const AddressPageTabbedSections: React.FC, , , + , ]; const accountAddressSections = @@ -209,12 +230,15 @@ export const AddressPageTabbedSections: React.FC = ({ delegationDetails }) => { + const { name: network, bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo); + const [pageNumber, setPageNumber] = useState(1); + const [currentPage, setCurrentPage] = useState([]); + + const totalAmount = delegationDetails?.reduce((acc, delegation) => acc + BigInt(delegation.output?.output?.amount ?? 0), BigInt(0)); + const totalRewards = delegationDetails?.reduce( + (acc, delegation) => acc + BigInt(delegation.rewards?.manaRewards?.rewards ?? 0), + BigInt(0), + ); + + useEffect(() => { + const from = (pageNumber - 1) * PAGE_SIZE; + const to = from + PAGE_SIZE; + if (delegationDetails) { + setCurrentPage(delegationDetails.slice(from, to)); + } + }, [delegationDetails, pageNumber]); + + if (delegationDetails === null) { + return null; + } + + return ( +
+
+
+
Total amount
+
{totalAmount?.toString()}
+
+
+
Total rewards
+
{totalRewards?.toString()}
+
+
+ + + + + + + + + + + + {currentPage.map((delegation, k) => { + const outputId = delegation.output.metadata.outputId; + const validatorAddress = Utils.addressToBech32( + (delegation.output?.output as DelegationOutput).validatorAddress, + bech32Hrp, + ); + + return ( + + + + + + + ); + })} + +
Output IdValidator addressAmountRewards
+ + + + {delegation.output?.output.amount.toString() ?? "-"}{delegation.rewards?.manaRewards?.rewards.toString() ?? "-"}
+ + {/* Only visible in mobile*/} +
+ {currentPage.map((delegation, k) => { + const outputId = delegation.output.metadata.outputId; + const validatorAddress = Utils.addressToBech32( + (delegation.output?.output as DelegationOutput).validatorAddress, + bech32Hrp, + ); + + return ( +
+
+
Output Id
+
+ +
+
+
+
Validator Address
+
+ +
+
+
+
Amount
+
{delegation.output?.output.amount.toString() ?? "-"}
+
+
+
Rewards
+
{delegation.rewards?.manaRewards?.rewards.toString() ?? "-"}
+
+
+ ); + })} +
+ setPageNumber(number)} + /> +
+ ); +}; + +export default DelegationSection; diff --git a/client/src/assets/modals/nova/delegation.json b/client/src/assets/modals/nova/delegation.json new file mode 100644 index 000000000..39c8143e1 --- /dev/null +++ b/client/src/assets/modals/nova/delegation.json @@ -0,0 +1,11 @@ +{ + "title": "Delegation", + "description": "

Delegators are token holders that contribute to consensus by delegating their voting power to validators of their choice.

", + "links": [ + { + "label": "Read more", + "href": "https://wiki.iota.org/learn/protocols/iota2.0/core-concepts/mana/#delegation", + "isExternal": true + } + ] +} diff --git a/client/src/helpers/nova/hooks/useAccountAddressState.ts b/client/src/helpers/nova/hooks/useAccountAddressState.ts index 48d414cfc..497656170 100644 --- a/client/src/helpers/nova/hooks/useAccountAddressState.ts +++ b/client/src/helpers/nova/hooks/useAccountAddressState.ts @@ -22,8 +22,10 @@ import { useAccountControlledFoundries } from "./useAccountControlledFoundries"; import { useAccountCongestion } from "./useAccountCongestion"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; import { useAccountValidatorDetails } from "./useAccountValidatorDetails"; +import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; import { useOutputManaRewards } from "./useOutputManaRewards"; +import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; export interface IAccountAddressState { addressDetails: IAddressDetails | null; @@ -38,12 +40,14 @@ export interface IAccountAddressState { validatorDetails: ValidatorResponse | null; addressBasicOutputs: OutputWithMetadataResponse[] | null; addressNftOutputs: OutputWithMetadataResponse[] | null; + addressDelegationOutputs: IDelegationWithDetails[] | null; foundries: string[] | null; congestion: CongestionResponse | null; isAccountDetailsLoading: boolean; isAssociatedOutputsLoading: boolean; isBasicOutputsLoading: boolean; isNftOutputsLoading: boolean; + isDelegationOutputsLoading: boolean; isFoundriesLoading: boolean; isAddressHistoryLoading: boolean; isAddressHistoryDisabled: boolean; @@ -64,12 +68,14 @@ const initialState = { validatorDetails: null, addressBasicOutputs: null, addressNftOutputs: null, + addressDelegationOutputs: null, foundries: null, congestion: null, isAccountDetailsLoading: true, isAssociatedOutputsLoading: false, isBasicOutputsLoading: false, isNftOutputsLoading: false, + isDelegationOutputsLoading: false, isFoundriesLoading: false, isAddressHistoryLoading: true, isAddressHistoryDisabled: false, @@ -105,6 +111,10 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres ); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); + const [addressDelegationOutputs, isDelegationOutputsLoading] = useAddressDelegationOutputs( + 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( @@ -138,8 +148,10 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres validatorDetails, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, isFoundriesLoading, isCongestionLoading, isValidatorDetailsLoading, @@ -179,11 +191,13 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres manaRewards, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, congestion, validatorDetails, isAccountDetailsLoading, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, isCongestionLoading, isValidatorDetailsLoading, ]); diff --git a/client/src/helpers/nova/hooks/useAddressDelegationOutputs.ts b/client/src/helpers/nova/hooks/useAddressDelegationOutputs.ts new file mode 100644 index 000000000..6ac8e0332 --- /dev/null +++ b/client/src/helpers/nova/hooks/useAddressDelegationOutputs.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import { ServiceFactory } from "~factories/serviceFactory"; +import { NOVA } from "~models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; +import { IDelegationWithDetails } from "~models/api/nova/IDelegationWithDetails"; + +/** + * Fetch Address delegation UTXOs + * @param network The Network in context + * @param addressBech32 The address in bech32 format + * @returns The output responses and loading bool. + */ +export function useAddressDelegationOutputs(network: string, addressBech32: string | null): [IDelegationWithDetails[] | null, boolean] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [outputs, setOutputs] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + setOutputs(null); + if (addressBech32) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .delegationOutputsDetails({ network, address: addressBech32 }) + .then((response) => { + if (!response?.error && response.outputs && isMounted) { + setOutputs(response.outputs); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, addressBech32]); + + return [outputs, isLoading]; +} diff --git a/client/src/helpers/nova/hooks/useAnchorAddressState.ts b/client/src/helpers/nova/hooks/useAnchorAddressState.ts index 17783514b..e214cfe68 100644 --- a/client/src/helpers/nova/hooks/useAnchorAddressState.ts +++ b/client/src/helpers/nova/hooks/useAnchorAddressState.ts @@ -9,19 +9,23 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; +import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; +import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; export interface IAnchorAddressState { addressDetails: IAddressDetails | null; anchorOutput: AnchorOutput | null; - addressBasicOutputs: OutputWithMetadataResponse[] | null; - addressNftOutputs: OutputWithMetadataResponse[] | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; totalManaBalance: IManaBalance | null; availableManaBalance: IManaBalance | null; + addressDelegationOutputs: IDelegationWithDetails[] | null; + addressBasicOutputs: OutputWithMetadataResponse[] | null; + addressNftOutputs: OutputWithMetadataResponse[] | null; isBasicOutputsLoading: boolean; isNftOutputsLoading: boolean; + isDelegationOutputsLoading: boolean; isAnchorDetailsLoading: boolean; isAssociatedOutputsLoading: boolean; isAddressHistoryLoading: boolean; @@ -37,8 +41,10 @@ const initialState = { availableManaBalance: null, addressBasicOutputs: null, addressNftOutputs: null, + addressDelegationOutputs: null, isBasicOutputsLoading: false, isNftOutputsLoading: false, + isDelegationOutputsLoading: false, isAnchorDetailsLoading: true, isAssociatedOutputsLoading: false, isAddressHistoryLoading: true, @@ -70,6 +76,10 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt ); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); + const [addressDelegationOutputs, isDelegationOutputsLoading] = useAddressDelegationOutputs( + network, + state.addressDetails?.bech32 ?? null, + ); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -92,8 +102,10 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt availableManaBalance, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, isAnchorDetailsLoading, }); }, [ @@ -104,8 +116,10 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt availableManaBalance, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, isAnchorDetailsLoading, ]); diff --git a/client/src/helpers/nova/hooks/useEd25519AddressState.ts b/client/src/helpers/nova/hooks/useEd25519AddressState.ts index 9739a3967..67936794a 100644 --- a/client/src/helpers/nova/hooks/useEd25519AddressState.ts +++ b/client/src/helpers/nova/hooks/useEd25519AddressState.ts @@ -7,18 +7,22 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; +import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; +import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; export interface IEd25519AddressState { addressDetails: IAddressDetails | null; - addressBasicOutputs: OutputWithMetadataResponse[] | null; - addressNftOutputs: OutputWithMetadataResponse[] | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; totalManaBalance: IManaBalance | null; availableManaBalance: IManaBalance | null; + addressBasicOutputs: OutputWithMetadataResponse[] | null; + addressNftOutputs: OutputWithMetadataResponse[] | null; + addressDelegationOutputs: IDelegationWithDetails[] | null; isBasicOutputsLoading: boolean; isNftOutputsLoading: boolean; + isDelegationOutputsLoading: boolean; isAssociatedOutputsLoading: boolean; isAddressHistoryLoading: boolean; isAddressHistoryDisabled: boolean; @@ -32,8 +36,10 @@ const initialState = { availableManaBalance: null, addressBasicOutputs: null, addressNftOutputs: null, + addressDelegationOutputs: null, isBasicOutputsLoading: false, isNftOutputsLoading: false, + isDelegationOutputsLoading: false, isAssociatedOutputsLoading: false, isAddressHistoryLoading: true, isAddressHistoryDisabled: false, @@ -61,6 +67,10 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres ); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); + const [addressDelegationOutputs, isDelegationOutputsLoading] = useAddressDelegationOutputs( + network, + state.addressDetails?.bech32 ?? null, + ); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -81,18 +91,22 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres availableManaBalance, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, }); }, [ - totalBaseTokenBalance, - availableBaseTokenBalance, totalManaBalance, availableManaBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, ]); return [state, setState]; diff --git a/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts b/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts index f50889f24..da1bab6b0 100644 --- a/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts +++ b/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts @@ -8,15 +8,19 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; +import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; +import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; export interface IImplicitAccountCreationAddressState { addressDetails: IAddressDetails | null; - addressBasicOutputs: OutputWithMetadataResponse[] | null; - addressNftOutputs: OutputWithMetadataResponse[] | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; + addressBasicOutputs: OutputWithMetadataResponse[] | null; + addressNftOutputs: OutputWithMetadataResponse[] | null; + addressDelegationOutputs: IDelegationWithDetails[] | null; isBasicOutputsLoading: boolean; isNftOutputsLoading: boolean; + isDelegationOutputsLoading: boolean; isAssociatedOutputsLoading: boolean; isAddressHistoryLoading: boolean; isAddressHistoryDisabled: boolean; @@ -28,8 +32,10 @@ const initialState = { availableBaseTokenBalance: null, addressBasicOutputs: null, addressNftOutputs: null, + addressDelegationOutputs: null, isBasicOutputsLoading: false, isNftOutputsLoading: false, + isDelegationOutputsLoading: false, isAssociatedOutputsLoading: false, isAddressHistoryLoading: true, isAddressHistoryDisabled: false, @@ -56,6 +62,10 @@ export const useImplicitAccountCreationAddressState = ( const { totalBaseTokenBalance, availableBaseTokenBalance } = useAddressBalance(network, state.addressDetails, null); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); + const [addressDelegationOutputs, isDelegationOutputsLoading] = useAddressDelegationOutputs( + network, + state.addressDetails?.bech32 ?? null, + ); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -75,16 +85,20 @@ export const useImplicitAccountCreationAddressState = ( availableBaseTokenBalance, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, }); }, [ totalBaseTokenBalance, availableBaseTokenBalance, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, - isBasicOutputsLoading, + isNftOutputsLoading, + isDelegationOutputsLoading, ]); return [state, setState]; diff --git a/client/src/helpers/nova/hooks/useNftAddressState.ts b/client/src/helpers/nova/hooks/useNftAddressState.ts index 3b5ffc2df..9c0c9ea4b 100644 --- a/client/src/helpers/nova/hooks/useNftAddressState.ts +++ b/client/src/helpers/nova/hooks/useNftAddressState.ts @@ -9,19 +9,23 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; +import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; +import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; export interface INftAddressState { addressDetails: IAddressDetails | null; nftOutput: NftOutput | null; - addressBasicOutputs: OutputWithMetadataResponse[] | null; - addressNftOutputs: OutputWithMetadataResponse[] | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; totalManaBalance: IManaBalance | null; availableManaBalance: IManaBalance | null; + addressBasicOutputs: OutputWithMetadataResponse[] | null; + addressNftOutputs: OutputWithMetadataResponse[] | null; + addressDelegationOutputs: IDelegationWithDetails[] | null; isBasicOutputsLoading: boolean; isNftOutputsLoading: boolean; + isDelegationOutputsLoading: boolean; isNftDetailsLoading: boolean; isAssociatedOutputsLoading: boolean; isAddressHistoryLoading: boolean; @@ -38,8 +42,10 @@ const initialState = { availableManaBalance: null, addressBasicOutputs: null, addressNftOutputs: null, + addressDelegationOutputs: null, isBasicOutputsLoading: false, isNftOutputsLoading: false, + isDelegationOutputsLoading: false, isAssociatedOutputsLoading: false, isAddressHistoryLoading: true, isAddressHistoryDisabled: false, @@ -70,6 +76,10 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac ); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); + const [addressDelegationOutputs, isDelegationOutputsLoading] = useAddressDelegationOutputs( + network, + state.addressDetails?.bech32 ?? null, + ); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -93,8 +103,10 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac isNftDetailsLoading, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, }); }, [ nftOutput, @@ -105,8 +117,10 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac isNftDetailsLoading, addressBasicOutputs, addressNftOutputs, + addressDelegationOutputs, isBasicOutputsLoading, isNftOutputsLoading, + isDelegationOutputsLoading, ]); return [state, setState]; diff --git a/client/src/models/api/nova/IDelegationDetailsResponse.ts b/client/src/models/api/nova/IDelegationDetailsResponse.ts new file mode 100644 index 000000000..4b2c22dfe --- /dev/null +++ b/client/src/models/api/nova/IDelegationDetailsResponse.ts @@ -0,0 +1,9 @@ +import { IResponse } from "./IResponse"; +import { IDelegationWithDetails } from "./IDelegationWithDetails"; + +export interface IDelegationDetailsResponse extends IResponse { + /** + * The outputs data. + */ + outputs?: IDelegationWithDetails[]; +} diff --git a/client/src/models/api/nova/IDelegationWithDetails.ts b/client/src/models/api/nova/IDelegationWithDetails.ts new file mode 100644 index 000000000..e1d0b728a --- /dev/null +++ b/client/src/models/api/nova/IDelegationWithDetails.ts @@ -0,0 +1,14 @@ +import { OutputWithMetadataResponse } from "@iota/sdk-wasm-nova/web"; +import { IRewardsResponse } from "./IRewardsResponse"; + +export interface IDelegationWithDetails { + /** + * The output. + */ + output: OutputWithMetadataResponse; + + /** + * The rewards for the output. + */ + rewards?: IRewardsResponse; +} diff --git a/client/src/scss/card.scss b/client/src/scss/card.scss index d848fa3f5..13acd1590 100644 --- a/client/src/scss/card.scss +++ b/client/src/scss/card.scss @@ -438,4 +438,8 @@ height: auto; } } + + &--no-border { + border: 0; + } } diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index c4d60f64c..297749d72 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -41,6 +41,7 @@ import { ICongestionResponse } from "~/models/api/nova/ICongestionResponse"; import { IAccountValidatorDetailsRequest } from "~/models/api/nova/IAccountValidatorDetailsRequest"; import { IAccountValidatorDetailsResponse } from "~/models/api/nova/IAccountValidatorDetailsResponse"; import { ILatestSlotCommitmentResponse } from "~/models/api/nova/ILatestSlotCommitmentsResponse"; +import { IDelegationDetailsResponse } from "~/models/api/nova/IDelegationDetailsResponse"; import { ISlotBlocksRequest } from "~/models/api/nova/ISlotBlocksRequest"; import { ISlotBlocksResponse } from "~/models/api/nova/ISlotBlocksResponse"; @@ -165,6 +166,18 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/address/outputs/nft/${request.network}/${request.address}`, "get"); } + /** + * Get the delegation outputs details of an address. + * @param request The Address Delegation outputs request. + * @returns The Address outputs response + */ + public async delegationOutputsDetails(request: IAddressDetailsRequest): Promise { + return this.callApi( + `nova/address/outputs/delegation/${request.network}/${request.address}`, + "get", + ); + } + /** * Get the associated outputs. * @param request The request to send.