Skip to content

Commit

Permalink
feat: Include output mana rewards in Output page nova
Browse files Browse the repository at this point in the history
  • Loading branch information
msarcev committed Jan 26, 2024
1 parent 09433ea commit d2c06e7
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 5 deletions.
11 changes: 11 additions & 0 deletions api/src/models/api/nova/IRewardsRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface IRewardsRequest {
/**
* The network to search on.
*/
network: string;

/**
* The output id to get the rewards for.
*/
outputId: string;
}
14 changes: 14 additions & 0 deletions api/src/models/api/nova/IRewardsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ManaRewardsResponse } from "@iota/sdk-nova";

Check failure on line 1 in api/src/models/api/nova/IRewardsResponse.ts

View workflow job for this annotation

GitHub Actions / lint-check (api)

Unable to resolve path to module '@iota/sdk-nova'
import { IResponse } from "./IResponse";

export interface IRewardsResponse extends IResponse {
/**
* The output Id.
*/
outputId?: string;

/**
* The output mana rewards.
*/
manaRewards?: ManaRewardsResponse;
}
1 change: 1 addition & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions api/src/routes/nova/output/rewards/get.ts
Original file line number Diff line number Diff line change
@@ -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<IRewardsResponse> {
const networkService = ServiceFactory.get<NetworkService>("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);
}
15 changes: 14 additions & 1 deletion api/src/services/nova/novaApi.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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<IRewardsResponse> {
const manaRewardsResponse = await this.tryFetchNodeThenPermanode<string, ManaRewardsResponse>(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).
Expand Down
20 changes: 18 additions & 2 deletions client/src/app/routes/nova/OutputPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -29,6 +30,7 @@ const OutputPage: React.FC<RouteComponentProps<OutputPageProps>> = ({
},
}) => {
const { output, outputMetadataResponse, error } = useOutputDetails(network, outputId);
const { manaRewards } = useOutputManaRewards(network, outputId);
const { protocolInfo, latestConfirmedSlot } = useNetworkInfoNova((s) => s.networkInfo);

if (error) {
Expand Down Expand Up @@ -58,9 +60,15 @@ const OutputPage: React.FC<RouteComponentProps<OutputPageProps>> = ({
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,
);
}
}

Expand Down Expand Up @@ -157,6 +165,14 @@ const OutputPage: React.FC<RouteComponentProps<OutputPageProps>> = ({
<span className="margin-r-t">{outputManaDetails.potentialMana}</span>
</div>
</div>
{outputManaDetails.delegationRewards && (
<div className="section--data">
<div className="label">Mana rewards</div>
<div className="value code row middle">
<span className="margin-r-t">{outputManaDetails.delegationRewards}</span>
</div>
</div>
)}
<div className="section--data">
<div className="label">Total mana</div>
<div className="value code row middle">
Expand Down
57 changes: 57 additions & 0 deletions client/src/helpers/nova/hooks/useOutputManaRewards.ts
Original file line number Diff line number Diff line change
@@ -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<NovaApiClient>(`api-client-${NOVA}`));
const [manaRewards, setManaRewards] = useState<ManaRewardsResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(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 };
}
12 changes: 10 additions & 2 deletions client/src/helpers/nova/manaUtils.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
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(
output: Output,
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,
};
}
16 changes: 16 additions & 0 deletions client/src/models/api/nova/IRewardsRequest.ts
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 14 additions & 0 deletions client/src/models/api/nova/IRewardsResponse.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions client/src/services/nova/novaApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<IRewardsResponse> {
return this.callApi<unknown, IRewardsResponse>(`nova/output/rewards/${request.network}/${request.outputId}`, "get");
}
}

0 comments on commit d2c06e7

Please sign in to comment.