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 delegation outputs to Validator tab of Account page #1340

Merged
merged 2 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/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
Loading