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: Support rewards mana on delegation output #1017

Merged
merged 8 commits into from
Jan 31, 2024
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");
}
}
Loading