From d2c06e7be0cfe517e03116b07dfb9f06d5c1d791 Mon Sep 17 00:00:00 2001 From: Mario Sarcevic Date: Fri, 26 Jan 2024 10:30:53 +0100 Subject: [PATCH] feat: Include output mana rewards in Output page nova --- api/src/models/api/nova/IRewardsRequest.ts | 11 ++++ api/src/models/api/nova/IRewardsResponse.ts | 14 +++++ api/src/routes.ts | 1 + api/src/routes/nova/output/rewards/get.ts | 29 ++++++++++ api/src/services/nova/novaApi.ts | 15 ++++- client/src/app/routes/nova/OutputPage.tsx | 20 ++++++- .../nova/hooks/useOutputManaRewards.ts | 57 +++++++++++++++++++ client/src/helpers/nova/manaUtils.ts | 12 +++- client/src/models/api/nova/IRewardsRequest.ts | 16 ++++++ .../src/models/api/nova/IRewardsResponse.ts | 14 +++++ client/src/services/nova/novaApiClient.ts | 11 ++++ 11 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 api/src/models/api/nova/IRewardsRequest.ts create mode 100644 api/src/models/api/nova/IRewardsResponse.ts create mode 100644 api/src/routes/nova/output/rewards/get.ts create mode 100644 client/src/helpers/nova/hooks/useOutputManaRewards.ts create mode 100644 client/src/models/api/nova/IRewardsRequest.ts create mode 100644 client/src/models/api/nova/IRewardsResponse.ts diff --git a/api/src/models/api/nova/IRewardsRequest.ts b/api/src/models/api/nova/IRewardsRequest.ts new file mode 100644 index 000000000..e1b9dd32f --- /dev/null +++ b/api/src/models/api/nova/IRewardsRequest.ts @@ -0,0 +1,11 @@ +export interface IRewardsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The output id to get the rewards for. + */ + outputId: string; +} diff --git a/api/src/models/api/nova/IRewardsResponse.ts b/api/src/models/api/nova/IRewardsResponse.ts new file mode 100644 index 000000000..e50b808a3 --- /dev/null +++ b/api/src/models/api/nova/IRewardsResponse.ts @@ -0,0 +1,14 @@ +import { ManaRewardsResponse } from "@iota/sdk-nova"; +import { IResponse } from "./IResponse"; + +export interface IRewardsResponse extends IResponse { + /** + * The output Id. + */ + outputId?: string; + + /** + * The output mana rewards. + */ + manaRewards?: ManaRewardsResponse; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index 08b59926b..91861a21b 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -203,6 +203,7 @@ export const routes: IRoute[] = [ }, // Nova { path: "/nova/output/:network/:outputId", method: "get", folder: "nova/output", func: "get" }, + { path: "/nova/output/rewards/:network/:outputId", method: "get", folder: "nova/output/rewards", func: "get" }, { path: "/nova/account/:network/:accountId", method: "get", folder: "nova/account", func: "get" }, { path: "/nova/output/associated/:network/:address", diff --git a/api/src/routes/nova/output/rewards/get.ts b/api/src/routes/nova/output/rewards/get.ts new file mode 100644 index 000000000..35768b7ba --- /dev/null +++ b/api/src/routes/nova/output/rewards/get.ts @@ -0,0 +1,29 @@ +import { ServiceFactory } from "../../../../factories/serviceFactory"; +import { IRewardsRequest } from "../../../../models/api/nova/IRewardsRequest"; +import { IRewardsResponse } from "../../../../models/api/nova/IRewardsResponse"; +import { IConfiguration } from "../../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../../models/db/protocolVersion"; +import { NetworkService } from "../../../../services/networkService"; +import { NovaApi } from "../../../../services/nova/novaApi"; +import { ValidationHelper } from "../../../../utils/validationHelper"; + +/** + * Get the output rewards. + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: IRewardsRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.outputId, "outputId"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + return NovaApi.getRewards(networkConfig, request.outputId); +} diff --git a/api/src/services/nova/novaApi.ts b/api/src/services/nova/novaApi.ts index 53ee46fb9..cadcb645c 100644 --- a/api/src/services/nova/novaApi.ts +++ b/api/src/services/nova/novaApi.ts @@ -1,12 +1,13 @@ /* eslint-disable import/no-unresolved */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { __ClientMethods__, OutputResponse, Client, Block, IBlockMetadata } from "@iota/sdk-nova"; +import { __ClientMethods__, OutputResponse, Client, Block, IBlockMetadata, ManaRewardsResponse } from "@iota/sdk-nova"; import { ServiceFactory } from "../../factories/serviceFactory"; import logger from "../../logger"; import { IAccountResponse } from "../../models/api/nova/IAccountResponse"; import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse"; import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; +import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse"; import { INetwork } from "../../models/db/INetwork"; import { HexHelper } from "../../utils/hexHelper"; @@ -89,6 +90,18 @@ export class NovaApi { return { message: "Account output not found" }; } + /** + * Get the output mana rewards. + * @param network The network to find the items on. + * @param outputId The outputId to get the rewards for. + * @returns The account details. + */ + public static async getRewards(network: INetwork, outputId: string): Promise { + const manaRewardsResponse = await this.tryFetchNodeThenPermanode(outputId, "getRewards", network); + + return manaRewardsResponse ? { outputId, manaRewards: manaRewardsResponse } : { outputId, message: "Rewards data not found" }; + } + /** * Generic helper function to try fetching from node client. * On failure (or not present), we try to fetch from permanode (if configured). diff --git a/client/src/app/routes/nova/OutputPage.tsx b/client/src/app/routes/nova/OutputPage.tsx index 8620ec3b9..0f858a0ad 100644 --- a/client/src/app/routes/nova/OutputPage.tsx +++ b/client/src/app/routes/nova/OutputPage.tsx @@ -10,6 +10,7 @@ import TruncatedId from "~/app/components/stardust/TruncatedId"; import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; import { buildManaDetailsForOutput, OutputManaDetails } from "~/helpers/nova/manaUtils"; import "./OutputPage.scss"; +import { useOutputManaRewards } from "~/helpers/nova/hooks/useOutputManaRewards"; interface OutputPageProps { /** @@ -29,6 +30,7 @@ const OutputPage: React.FC> = ({ }, }) => { const { output, outputMetadataResponse, error } = useOutputDetails(network, outputId); + const { manaRewards } = useOutputManaRewards(network, outputId); const { protocolInfo, latestConfirmedSlot } = useNetworkInfoNova((s) => s.networkInfo); if (error) { @@ -58,9 +60,15 @@ const OutputPage: React.FC> = ({ let outputManaDetails: OutputManaDetails | null = null; if (output !== null && createdSlotIndex !== null && protocolInfo !== null) { if (isSpent && spentSlotIndex !== null) { - outputManaDetails = buildManaDetailsForOutput(output, createdSlotIndex, spentSlotIndex, protocolInfo.parameters); + outputManaDetails = buildManaDetailsForOutput(output, createdSlotIndex, spentSlotIndex, protocolInfo.parameters, manaRewards); } else if (latestConfirmedSlot > 0) { - outputManaDetails = buildManaDetailsForOutput(output, createdSlotIndex, latestConfirmedSlot, protocolInfo.parameters); + outputManaDetails = buildManaDetailsForOutput( + output, + createdSlotIndex, + latestConfirmedSlot, + protocolInfo.parameters, + manaRewards, + ); } } @@ -157,6 +165,14 @@ const OutputPage: React.FC> = ({ {outputManaDetails.potentialMana} + {outputManaDetails.delegationRewards && ( +
+
Mana rewards
+
+ {outputManaDetails.delegationRewards} +
+
+ )}
Total mana
diff --git a/client/src/helpers/nova/hooks/useOutputManaRewards.ts b/client/src/helpers/nova/hooks/useOutputManaRewards.ts new file mode 100644 index 000000000..dd88da13c --- /dev/null +++ b/client/src/helpers/nova/hooks/useOutputManaRewards.ts @@ -0,0 +1,57 @@ +import { ManaRewardsResponse } 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 output mana rewards for a given output. + * @param network The Network in context + * @param outputId The output id + * @param slotIndex The slot index + * @returns The output, metadata, loading bool and error message. + **/ +export function useOutputManaRewards( + network: string, + outputId: string, + slotIndex?: number, +): { + manaRewards: ManaRewardsResponse | null; + isLoading: boolean; + error: string | null; +} { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [manaRewards, setManaRewards] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + setManaRewards(null); + setError(null); + + if (outputId) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .getRewards({ network, outputId, slotIndex }) + .then((response) => { + if (isMounted) { + const manaRewards = response.manaRewards; + setError(response.error ?? null); + setManaRewards(manaRewards ?? null); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, outputId, slotIndex]); + + return { manaRewards, isLoading, error }; +} diff --git a/client/src/helpers/nova/manaUtils.ts b/client/src/helpers/nova/manaUtils.ts index 62fb9dcb5..5dee2ba13 100644 --- a/client/src/helpers/nova/manaUtils.ts +++ b/client/src/helpers/nova/manaUtils.ts @@ -1,10 +1,11 @@ -import { BasicOutput, Output, ProtocolParameters, Utils } from "@iota/sdk-wasm-nova/web"; +import { BasicOutput, ManaRewardsResponse, Output, ProtocolParameters, Utils } from "@iota/sdk-wasm-nova/web"; export interface OutputManaDetails { storedMana: string; storedManaDecayed: string; potentialMana: string; totalMana: string; + delegationRewards?: string | null; } export function buildManaDetailsForOutput( @@ -12,16 +13,23 @@ export function buildManaDetailsForOutput( createdSlotIndex: number, spentOrLatestSlotIndex: number, protocolParameters: ProtocolParameters, + outputManaRewards: ManaRewardsResponse | null, ): OutputManaDetails { const decayedMana = Utils.outputManaWithDecay(output, createdSlotIndex, spentOrLatestSlotIndex, protocolParameters); const storedManaDecayed = BigInt(decayedMana.stored).toString(); const potentialMana = BigInt(decayedMana.potential).toString(); - const totalMana = BigInt(decayedMana.stored) + BigInt(decayedMana.potential); + const delegationRewards = outputManaRewards && BigInt(outputManaRewards?.rewards) > 0 ? BigInt(outputManaRewards?.rewards) : null; + let totalMana = BigInt(decayedMana.stored) + BigInt(decayedMana.potential); + + if (delegationRewards !== null) { + totalMana += delegationRewards; + } return { storedMana: (output as BasicOutput).mana?.toString(), storedManaDecayed, potentialMana, totalMana: totalMana.toString(), + delegationRewards: delegationRewards !== null ? delegationRewards?.toString() : undefined, }; } diff --git a/client/src/models/api/nova/IRewardsRequest.ts b/client/src/models/api/nova/IRewardsRequest.ts new file mode 100644 index 000000000..05755f980 --- /dev/null +++ b/client/src/models/api/nova/IRewardsRequest.ts @@ -0,0 +1,16 @@ +export interface IRewardsRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The output id to get the rewards for. + */ + outputId: string; + + /** + * The slot index to use. + */ + slotIndex?: number; +} diff --git a/client/src/models/api/nova/IRewardsResponse.ts b/client/src/models/api/nova/IRewardsResponse.ts new file mode 100644 index 000000000..d5fb13ab4 --- /dev/null +++ b/client/src/models/api/nova/IRewardsResponse.ts @@ -0,0 +1,14 @@ +import { ManaRewardsResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "./IResponse"; + +export interface IRewardsResponse extends IResponse { + /** + * The output Id. + */ + outputId: string; + + /** + * The output mana rewards. + */ + manaRewards?: ManaRewardsResponse; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index c04f4b336..4dc56ab8f 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -11,6 +11,8 @@ import { IAssociationsRequest } from "~/models/api/stardust/IAssociationsRequest import { ApiClient } from "../apiClient"; import { IBlockDetailsRequest } from "~/models/api/nova/block/IBlockDetailsRequest"; import { IBlockDetailsResponse } from "~/models/api/nova/block/IBlockDetailsResponse"; +import { IRewardsRequest } from "~/models/api/nova/IRewardsRequest"; +import { IRewardsResponse } from "~/models/api/nova/IRewardsResponse"; /** * Class to handle api communications on nova. @@ -73,4 +75,13 @@ export class NovaApiClient extends ApiClient { { addressDetails: request.addressDetails }, ); } + + /** + * Get the output mana rewards. + * @param request The request to send. + * @returns The response from the request. + */ + public async getRewards(request: IRewardsRequest): Promise { + return this.callApi(`nova/output/rewards/${request.network}/${request.outputId}`, "get"); + } }