Skip to content

Commit

Permalink
Feat: Support rewards mana on delegation output (#1017)
Browse files Browse the repository at this point in the history
* feat: Add mana details to metadata section of Output page (stored with decay, potential, total) for spent and unspent outputs

* feat: Include output mana rewards in Output page nova

* feat: Cleanup IRewardsRequest interface

* chore: Ignore more eslint import unresolved (nova CI)

* fix: comments

---------

Co-authored-by: Branko Bosnic <[email protected]>
  • Loading branch information
msarcev and brancoder authored Jan 31, 2024
1 parent 6cadacd commit 0c85efc
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 3 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;
}
16 changes: 16 additions & 0 deletions api/src/models/api/nova/IRewardsResponse.ts
Original file line number Diff line number Diff line change
@@ -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;
}
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
30 changes: 30 additions & 0 deletions api/src/routes/nova/output/rewards/get.ts
Original file line number Diff line number Diff line change
@@ -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<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 {};
}

const novaApiService = ServiceFactory.get<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.getRewards(request.outputId);
}
12 changes: 12 additions & 0 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<IRewardsResponse> {
const manaRewardsResponse = await this.client.getRewards(outputId);

return manaRewardsResponse ? { outputId, manaRewards: manaRewardsResponse } : { outputId, message: "Rewards data not found" };
}
}
12 changes: 11 additions & 1 deletion 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 { Converter } from "~/helpers/stardust/convertUtils";
import { useOutputManaRewards } from "~/helpers/nova/hooks/useOutputManaRewards";
import "./OutputPage.scss";

interface OutputPageProps {
Expand All @@ -30,6 +31,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 @@ -62,7 +64,7 @@ const OutputPage: React.FC<RouteComponentProps<OutputPageProps>> = ({
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;
}

Expand Down Expand Up @@ -159,6 +161,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 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<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 })
.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,
delegationRewards: delegationRewards !== null ? delegationRewards?.toString() : undefined,
totalMana: totalMana.toString(),
};
}
11 changes: 11 additions & 0 deletions client/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;
}
16 changes: 16 additions & 0 deletions client/src/models/api/nova/IRewardsResponse.ts
Original file line number Diff line number Diff line change
@@ -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;
}
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 0c85efc

Please sign in to comment.