Skip to content

Commit

Permalink
feat: Add delegation output to Validator tab of Account page (the out…
Browse files Browse the repository at this point in the history
…put that are delegated to the address) (#1340)

Co-authored-by: Begoña Alvarez <[email protected]>
  • Loading branch information
msarcev and begonaalvarezd authored Mar 26, 2024
1 parent 0c5eac5 commit b33071a
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 6 deletions.
11 changes: 11 additions & 0 deletions api/src/models/api/nova/IDelegationByValidatorResponse.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 { OutputWithMetadataResponse } from "@iota/sdk-nova";
import { IResponse } from "./IResponse";

export interface IDelegationByValidatorResponse extends IResponse {
/**
* The delegation output by validator address.
*/
outputs?: OutputWithMetadataResponse[];
}
6 changes: 6 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ export const routes: IRoute[] = [
func: "post",
dataBody: true,
},
{
path: "/nova/output/delegation/by-validator/:network/:address",
method: "get",
folder: "nova/output/delegation/by-validator",
func: "get",
},
{
path: "/nova/account/foundries/:network/:accountAddress",
method: "get",
Expand Down
2 changes: 1 addition & 1 deletion api/src/routes/nova/address/outputs/delegation/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { NovaApiService } from "../../../../../services/nova/novaApiService";
import { ValidationHelper } from "../../../../../utils/validationHelper";

/**
* Fetch the delegation output details by address.
* Fetch the delegation outputs by owning address.
* @param config The configuration.
* @param request The request.
* @returns The response.
Expand Down
30 changes: 30 additions & 0 deletions api/src/routes/nova/output/delegation/by-validator/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServiceFactory } from "../../../../../factories/serviceFactory";
import { IAddressDetailsRequest } from "../../../../../models/api/nova/IAddressDetailsRequest";
import { IDelegationByValidatorResponse } from "../../../../../models/api/nova/IDelegationByValidatorResponse";
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 outputs by owning address.
* @param _ The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(_: IConfiguration, request: IAddressDetailsRequest): Promise<IDelegationByValidatorResponse> {
const networkService = ServiceFactory.get<NetworkService>("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<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.delegationOutputDetailsByValidator(request.address);
}
30 changes: 30 additions & 0 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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 { IDelegationByValidatorResponse } from "../../models/api/nova/IDelegationByValidatorResponse";
import { IDelegationDetailsResponse } from "../../models/api/nova/IDelegationDetailsResponse";
import { IDelegationWithDetails } from "../../models/api/nova/IDelegationWithDetails";
import { IEpochCommitteeResponse } from "../../models/api/nova/IEpochCommitteeResponse";
Expand Down Expand Up @@ -405,6 +406,7 @@ export class NovaApiService {

/**
* Get the relevant delegation output details for an address.
* Return the output this address is delegating.
* @param addressBech32 The address in bech32 format.
* @returns The delegation output details.
*/
Expand Down Expand Up @@ -437,6 +439,34 @@ export class NovaApiService {
};
}

/**
* Get the delegation outputs that this address has been delegated to.
* Return the outputs for which this address is the 'validator'.
* @param addressBech32 The address in bech32 format.
* @returns The delegation output details.
*/
public async delegationOutputDetailsByValidator(addressBech32: string): Promise<IDelegationByValidatorResponse> {
let cursor: string | undefined;
let outputIds: string[] = [];

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

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

const outputResponses = await this.outputsDetails(outputIds);

return {
outputs: outputResponses,
};
}

/**
* Get Congestion for Account
* @param accountId The account address to get the congestion for.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const buildAccountAddressTabsOptions = (
hasStakingFeature: boolean,
isAccountFoundriesLoading: boolean,
isValidatorDetailsLoading: boolean,
isValidatorDelegationOutputsLoading: boolean,
) => ({
[ACCOUNT_TABS.BlockIssuance]: {
disabled: !isBlockIssuer,
Expand All @@ -118,7 +119,7 @@ const buildAccountAddressTabsOptions = (
[ACCOUNT_TABS.Validation]: {
disabled: !hasStakingFeature,
hidden: !hasStakingFeature,
isLoading: isValidatorDetailsLoading,
isLoading: isValidatorDetailsLoading || isValidatorDelegationOutputsLoading,
infoContent: validatorMessage,
},
});
Expand Down Expand Up @@ -212,6 +213,7 @@ export const AddressPageTabbedSections: React.FC<IAddressPageTabbedSectionsProps
<AccountValidatorSection
key={`account-validator-${addressBech32}`}
validatorDetails={(addressState as IAccountAddressState).validatorDetails}
validatorDelegationOutputs={(addressState as IAccountAddressState).validatorDelegationOutputs}
/>,
]
: null;
Expand Down Expand Up @@ -256,6 +258,7 @@ export const AddressPageTabbedSections: React.FC<IAddressPageTabbedSectionsProps
accountAddressState.stakingFeature !== null,
accountAddressState.isFoundriesLoading,
accountAddressState.isValidatorDetailsLoading,
accountAddressState.isValidatorDelegationOutputsLoading,
),
};
tabbedSections = [...defaultSections, ...(accountAddressSections ?? [])];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import React from "react";
import { ValidatorResponse } from "@iota/sdk-wasm-nova/web";
import React, { useState } from "react";
import { OutputWithMetadataResponse, ValidatorResponse } from "@iota/sdk-wasm-nova/web";
import TruncatedId from "~/app/components/stardust/TruncatedId";
import { useNetworkInfoNova } from "~/helpers/nova/networkInfo";
import { formatAmount } from "~/helpers/stardust/valueFormatHelper";

interface AccountValidatorSectionProps {
readonly validatorDetails: ValidatorResponse | null;
readonly validatorDelegationOutputs: OutputWithMetadataResponse[] | null;
}

const AccountValidatorSection: React.FC<AccountValidatorSectionProps> = ({ validatorDetails }) => {
const AccountValidatorSection: React.FC<AccountValidatorSectionProps> = ({ validatorDetails, validatorDelegationOutputs }) => {
if (!validatorDetails) {
return null;
}

const [isFormatBalance, setIsFormatBalance] = useState<boolean>(false);
const { name: network, tokenInfo } = useNetworkInfoNova((state) => state.networkInfo);

const delegatedStake = BigInt(validatorDetails.poolStake) - BigInt(validatorDetails.validatorStake);

return (
Expand Down Expand Up @@ -47,6 +54,23 @@ const AccountValidatorSection: React.FC<AccountValidatorSectionProps> = ({ valid
<div className="card--label margin-b-t">Latest Supported Protocol Hash</div>
<div className="card--value">{validatorDetails?.latestSupportedProtocolHash}</div>
</div>
{validatorDelegationOutputs && (
<div className="field">
<div className="card--label margin-b-t">Delegated outputs</div>
{validatorDelegationOutputs?.map((output, index) => (
<div key={index} className="card--value row">
<TruncatedId
id={output.metadata.outputId}
link={`/${network}/output/${output.metadata.outputId}`}
showCopyButton={true}
/>
<span onClick={() => setIsFormatBalance(!isFormatBalance)} className="pointer margin-l-t">
{formatAmount(output.output.amount, tokenInfo, isFormatBalance)}
</span>
</div>
))}
</div>
)}
</div>
</div>
);
Expand Down
15 changes: 15 additions & 0 deletions client/src/helpers/nova/hooks/useAccountAddressState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs";
import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse";
import { useOutputManaRewards } from "./useOutputManaRewards";
import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails";
import { useValidatorDelegationOutputs } from "./useValidatorDelegationOutputs";

export interface IAccountAddressState {
addressDetails: IAddressDetails | null;
Expand All @@ -41,14 +42,18 @@ export interface IAccountAddressState {
validatorDetails: ValidatorResponse | null;
addressBasicOutputs: OutputWithMetadataResponse[] | null;
addressNftOutputs: OutputWithMetadataResponse[] | null;
// This address is delegating output
addressDelegationOutputs: IDelegationWithDetails[] | null;
// This address is being delegated outputs to
validatorDelegationOutputs: OutputWithMetadataResponse[] | null;
foundries: string[] | null;
congestion: CongestionResponse | null;
isAccountDetailsLoading: boolean;
isAssociatedOutputsLoading: boolean;
isBasicOutputsLoading: boolean;
isNftOutputsLoading: boolean;
isDelegationOutputsLoading: boolean;
isValidatorDelegationOutputsLoading: boolean;
isFoundriesLoading: boolean;
isAddressHistoryLoading: boolean;
isAddressHistoryDisabled: boolean;
Expand All @@ -71,13 +76,15 @@ const initialState = {
addressBasicOutputs: null,
addressNftOutputs: null,
addressDelegationOutputs: null,
validatorDelegationOutputs: null,
foundries: null,
congestion: null,
isAccountDetailsLoading: true,
isAssociatedOutputsLoading: false,
isBasicOutputsLoading: false,
isNftOutputsLoading: false,
isDelegationOutputsLoading: false,
isValidatorDelegationOutputsLoading: false,
isFoundriesLoading: false,
isAddressHistoryLoading: true,
isAddressHistoryDisabled: false,
Expand Down Expand Up @@ -111,6 +118,10 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres
network,
state.addressDetails?.bech32 ?? null,
);
const [validatorDelegationOutputs, isValidatorDelegationOutputsLoading] = useValidatorDelegationOutputs(
network,
state.addressDetails?.bech32 ?? null,
);
const { congestion, isLoading: isCongestionLoading } = useAccountCongestion(network, state.addressDetails?.hex ?? null);
const { validatorDetails, isLoading: isValidatorDetailsLoading } = useAccountValidatorDetails(
network,
Expand Down Expand Up @@ -158,9 +169,11 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres
addressBasicOutputs,
addressNftOutputs,
addressDelegationOutputs,
validatorDelegationOutputs,
isBasicOutputsLoading,
isNftOutputsLoading,
isDelegationOutputsLoading,
isValidatorDelegationOutputsLoading,
isFoundriesLoading,
isCongestionLoading,
isValidatorDetailsLoading,
Expand Down Expand Up @@ -216,12 +229,14 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres
addressNftOutputs,
accountFoundryOutputs,
addressDelegationOutputs,
validatorDelegationOutputs,
congestion,
validatorDetails,
isAccountDetailsLoading,
isBasicOutputsLoading,
isNftOutputsLoading,
isDelegationOutputsLoading,
isValidatorDelegationOutputsLoading,
isCongestionLoading,
isValidatorDetailsLoading,
]);
Expand Down
46 changes: 46 additions & 0 deletions client/src/helpers/nova/hooks/useValidatorDelegationOutputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 { OutputWithMetadataResponse } from "@iota/sdk-wasm-nova/web";

/**
* Fetch delegation outputs this address is delegated to (validator).
* @param network The Network in context
* @param addressBech32 The address in bech32 format
* @returns The output responses and loading bool.
*/
export function useValidatorDelegationOutputs(
network: string,
addressBech32: string | null,
): [OutputWithMetadataResponse[] | null, boolean] {
const isMounted = useIsMounted();
const [apiClient] = useState(ServiceFactory.get<NovaApiClient>(`api-client-${NOVA}`));
const [outputs, setOutputs] = useState<OutputWithMetadataResponse[] | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);

useEffect(() => {
setIsLoading(true);
setOutputs(null);
if (addressBech32) {
// eslint-disable-next-line no-void
void (async () => {
apiClient
.delegationOutputsByValidator({ 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];
}
9 changes: 9 additions & 0 deletions client/src/models/api/nova/IDelegationByValidatorResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { OutputWithMetadataResponse } from "@iota/sdk-wasm-nova/web";
import { IResponse } from "./IResponse";

export interface IDelegationByValidatorResponse extends IResponse {
/**
* The delegation output by validator address.
*/
outputs?: OutputWithMetadataResponse[];
}
13 changes: 13 additions & 0 deletions client/src/services/nova/novaApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { ITaggedOutputsRequest } from "~/models/api/nova/ITaggedOutputsRequest";
import { IEpochAnalyticStats } from "~/models/api/nova/stats/IEpochAnalyticStats";
import { IEpochAnalyticStatsRequest } from "~/models/api/nova/stats/IEpochAnalyticStatsRequest";
import { IValidatorStatsResponse } from "~/models/api/nova/IValidatorStatsResponse";
import { IDelegationByValidatorResponse } from "~/models/api/nova/IDelegationByValidatorResponse";
import { ISlotManaBurnedRequest } from "~/models/api/nova/stats/ISlotManaBurnedRequest";
import { ISlotManaBurnedResponse } from "~/models/api/nova/stats/ISlotManaBurnedResponse";

Expand Down Expand Up @@ -212,6 +213,18 @@ export class NovaApiClient extends ApiClient {
);
}

/**
* Get the delegation outputs details of an address.
* @param request The Address Delegation outputs request.
* @returns The Address outputs response
*/
public async delegationOutputsByValidator(request: IAddressDetailsRequest): Promise<IDelegationByValidatorResponse> {
return this.callApi<unknown, IDelegationByValidatorResponse>(
`nova/output/delegation/by-validator/${request.network}/${request.address}`,
"get",
);
}

/**
* Get the associated outputs.
* @param request The request to send.
Expand Down
2 changes: 1 addition & 1 deletion client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
],
"~features/*": [
"src/features/*"
],
]
}
},
"include": [
Expand Down

0 comments on commit b33071a

Please sign in to comment.