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..587c6dc14 --- /dev/null +++ b/api/src/models/api/nova/IRewardsResponse.ts @@ -0,0 +1,16 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +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..c9d0fe427 --- /dev/null +++ b/api/src/routes/nova/output/rewards/get.ts @@ -0,0 +1,30 @@ +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 { NovaApiService } from "../../../../services/nova/novaApiService"; +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 {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.getRewards(request.outputId); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 5fcd200ba..27f6406b2 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -7,6 +7,7 @@ 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"; @@ -102,4 +103,15 @@ export class NovaApiService { return { message: "Account output not found" }; } } + + /** + * Get the output mana rewards. + * @param outputId The outputId to get the rewards for. + * @returns The mana rewards. + */ + public async getRewards(outputId: string): Promise { + const manaRewardsResponse = await this.client.getRewards(outputId); + + return manaRewardsResponse ? { outputId, manaRewards: manaRewardsResponse } : { outputId, message: "Rewards data not found" }; + } } diff --git a/client/src/app/routes/nova/OutputPage.tsx b/client/src/app/routes/nova/OutputPage.tsx index b60844474..32ba9592c 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 { Converter } from "~/helpers/stardust/convertUtils"; +import { useOutputManaRewards } from "~/helpers/nova/hooks/useOutputManaRewards"; import "./OutputPage.scss"; interface OutputPageProps { @@ -30,6 +31,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) { @@ -62,7 +64,7 @@ const OutputPage: React.FC> = ({ if (output && createdSlotIndex && protocolInfo) { const untilSlotIndex = spentSlotIndex ? spentSlotIndex : latestConfirmedSlot > 0 ? latestConfirmedSlot : null; outputManaDetails = untilSlotIndex - ? buildManaDetailsForOutput(output, createdSlotIndex, untilSlotIndex, protocolInfo.parameters) + ? buildManaDetailsForOutput(output, createdSlotIndex, untilSlotIndex, protocolInfo.parameters, manaRewards) : null; } @@ -159,6 +161,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..07f6c0029 --- /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 mana rewards, 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 }) + .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..255c013ea 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, + delegationRewards: delegationRewards !== null ? delegationRewards?.toString() : undefined, totalMana: totalMana.toString(), }; } diff --git a/client/src/models/api/nova/IRewardsRequest.ts b/client/src/models/api/nova/IRewardsRequest.ts new file mode 100644 index 000000000..e1b9dd32f --- /dev/null +++ b/client/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/client/src/models/api/nova/IRewardsResponse.ts b/client/src/models/api/nova/IRewardsResponse.ts new file mode 100644 index 000000000..2ad5deab9 --- /dev/null +++ b/client/src/models/api/nova/IRewardsResponse.ts @@ -0,0 +1,16 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +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"); + } }