Skip to content

Commit

Permalink
Feat: Add burned mana to slot (#1317)
Browse files Browse the repository at this point in the history
* feat: Add novaTimeService for convenience and use it.

* feat: Add route (and influx support) to get slot burned mana. Add client hook and use it on slot page.

* chore: Silence eslint 'unresolved' on CI (api)

* feat: Cache last 20 ManaBurned requests fetched on api

* fix: Fix formatAmount for '0' values. Add tests for it.

* feat: Ensure mana burned is shown on slot page

* feat: Add burned mana to Slot feed on Landing

* fix: Fix field name in novaTimeService

Co-authored-by: JCNoguera <[email protected]>

---------

Co-authored-by: JCNoguera <[email protected]>
Co-authored-by: Branko Bosnic <[email protected]>
  • Loading branch information
3 people authored Mar 26, 2024
1 parent 248d915 commit 564ea92
Show file tree
Hide file tree
Showing 19 changed files with 381 additions and 43 deletions.
31 changes: 20 additions & 11 deletions api/src/initServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { NovaFeed } from "./services/nova/feed/novaFeed";
import { InfluxServiceNova } from "./services/nova/influx/influxServiceNova";
import { NodeInfoService as NodeInfoServiceNova } from "./services/nova/nodeInfoService";
import { NovaApiService } from "./services/nova/novaApiService";
import { NovaTimeService } from "./services/nova/novaTimeService";
import { NovaStatsService } from "./services/nova/stats/novaStatsService";
import { ValidatorService } from "./services/nova/validatorService";
import { ChronicleService as ChronicleServiceStardust } from "./services/stardust/chronicleService";
Expand Down Expand Up @@ -246,21 +247,29 @@ function initNovaServices(networkConfig: INetwork): void {
ServiceFactory.register(`feed-${networkConfig.network}`, () => novaFeed);
});

NovaTimeService.build(novaClient)
.then((novaTimeService) => {
ServiceFactory.register(`nova-time-${networkConfig.network}`, () => novaTimeService);

const influxDBService = new InfluxServiceNova(networkConfig);
influxDBService
.buildClient()
.then((hasClient) => {
logger.debug(`[InfluxDb] Registering client with name "${networkConfig.network}". Has client: ${hasClient}`);
if (hasClient) {
ServiceFactory.register(`influxdb-${networkConfig.network}`, () => influxDBService);
}
})
.catch((e) => logger.error(`Failed to build influxDb client for "${networkConfig.network}". Cause: ${e}`));
})
.catch((err) => {
logger.error(`Failed building [novaTimeService]. Cause: ${err}`);
});

const validatorService = new ValidatorService(networkConfig);
validatorService.setupValidatorsCollection();
ServiceFactory.register(`validator-service-${networkConfig.network}`, () => validatorService);
});

const influxDBService = new InfluxServiceNova(networkConfig);
influxDBService
.buildClient()
.then((hasClient) => {
logger.debug(`[InfluxDb] Registering client with name "${networkConfig.network}". Has client: ${hasClient}`);
if (hasClient) {
ServiceFactory.register(`influxdb-${networkConfig.network}`, () => influxDBService);
}
})
.catch((e) => logger.warn(`Failed to build influxDb client for "${networkConfig.network}". Cause: ${e}`));
}

/**
Expand Down
11 changes: 11 additions & 0 deletions api/src/models/api/nova/stats/slot/ISlotManaBurnedRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ISlotManaBurnedRequest {
/**
* The network to search on.
*/
network: string;

/**
* The slot index to get the mana burned for.
*/
slotIndex: string;
}
6 changes: 6 additions & 0 deletions api/src/models/api/nova/stats/slot/ISlotManaBurnedResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IResponse } from "../../IResponse";

export interface ISlotManaBurnedResponse extends IResponse {
slotIndex?: number;
manaBurned?: number;
}
12 changes: 11 additions & 1 deletion api/src/models/influx/nova/IInfluxDbCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
IUnlockConditionsPerTypeDailyInflux,
IValidatorsActivityDailyInflux,
} from "./IInfluxTimedEntries";
import { DayKey } from "../types";
import { DayKey, ITimedEntry } from "../types";

/**
* The cache for influx graphs (daily).
Expand Down Expand Up @@ -79,6 +79,16 @@ interface IEpochAnalyticStats {
*/
export type IInfluxEpochAnalyticsCache = Map<number, IEpochAnalyticStats>;

export type ManaBurnedInSlot = ITimedEntry & {
slotIndex: number;
manaBurned: number;
};

/**
* The epoch stats cache. Map epoch index to stats.
*/
export type ManaBurnedInSlotCache = Map<number, ManaBurnedInSlot>;

/**
* The helper to initialize empty maps
* @returns The initial cache object
Expand Down
1 change: 1 addition & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export const routes: IRoute[] = [
},
{ path: "/nova/slot/:network/:slotIndex", method: "get", folder: "nova/slot", func: "get" },
{ path: "/nova/epoch/committee/:network/:epochIndex", method: "get", folder: "nova/epoch/committee", func: "get" },
{ path: "/nova/slot/mana-burned/:network/:slotIndex", method: "get", folder: "nova/slot/mana-burned", func: "get" },
{
path: "/nova/analytics/:network",
method: "get",
Expand Down
5 changes: 1 addition & 4 deletions api/src/routes/nova/epoch/influx/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ export async function get(_: IConfiguration, request: IEpochAnalyticStatsRequest
}

const epochIndex = Number.parseInt(request.epochIndex, 10);
let maybeEpochStats = influxService.getEpochAnalyticStats(epochIndex);
if (!maybeEpochStats) {
maybeEpochStats = await influxService.fetchAnalyticsForEpoch(epochIndex, protocolParameters);
}
const maybeEpochStats = await influxService.getEpochAnalyticStats(epochIndex);

return maybeEpochStats
? {
Expand Down
45 changes: 45 additions & 0 deletions api/src/routes/nova/slot/mana-burned/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ServiceFactory } from "../../../../factories/serviceFactory";
import { ISlotManaBurnedRequest } from "../../../../models/api/nova/stats/slot/ISlotManaBurnedRequest";
import { ISlotManaBurnedResponse } from "../../../../models/api/nova/stats/slot/ISlotManaBurnedResponse";
import { IConfiguration } from "../../../../models/configuration/IConfiguration";
import { NOVA } from "../../../../models/db/protocolVersion";
import { NetworkService } from "../../../../services/networkService";
import { InfluxServiceNova } from "../../../../services/nova/influx/influxServiceNova";
import { ValidationHelper } from "../../../../utils/validationHelper";

/**
* Fetch the slot mana burned from influx nova.
* @param _ The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(_: IConfiguration, request: ISlotManaBurnedRequest): Promise<ISlotManaBurnedResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.numberFromString(request.slotIndex, "slotIndex");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const influxService = ServiceFactory.get<InfluxServiceNova>(`influxdb-${networkConfig.network}`);

if (influxService) {
const slotIndex = Number.parseInt(request.slotIndex, 10);
const manaBurnedInSlot = await influxService.getManaBurnedBySlotIndex(slotIndex);

if (manaBurnedInSlot) {
return {
slotIndex: manaBurnedInSlot.slotIndex,
manaBurned: manaBurnedInSlot.manaBurned,
};
}

return { error: `Could not fetch mana burned for slot ${request.slotIndex}` };
}

return { error: "Influx service not found for this network." };
}
107 changes: 89 additions & 18 deletions api/src/services/nova/influx/influxServiceNova.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable import/no-unresolved */
import { ProtocolParameters } from "@iota/sdk-nova";
import { InfluxDB, toNanoDate } from "influx";
import moment from "moment";
import cron from "node-cron";
Expand Down Expand Up @@ -35,13 +34,14 @@ import {
} from "./influxQueries";
import { ServiceFactory } from "../../../factories/serviceFactory";
import logger from "../../../logger";
import { IEpochAnalyticStats } from "../../../models/api/nova/stats/epoch/IEpochAnalyticStats";
import { INetwork } from "../../../models/db/INetwork";
import {
IInfluxAnalyticsCache,
IInfluxDailyCache,
IInfluxEpochAnalyticsCache,
initializeEmptyDailyCache,
ManaBurnedInSlot,
ManaBurnedInSlotCache,
} from "../../../models/influx/nova/IInfluxDbCache";
import {
IAccountActivityDailyInflux,
Expand All @@ -68,9 +68,8 @@ import {
IValidatorsActivityDailyInflux,
} from "../../../models/influx/nova/IInfluxTimedEntries";
import { ITimedEntry } from "../../../models/influx/types";
import { epochIndexToUnixTimeRangeConverter, unixTimestampToEpochIndexConverter } from "../../../utils/nova/novaTimeUtils";
import { InfluxDbClient, NANOSECONDS_IN_MILLISECOND } from "../../influx/influxClient";
import { NodeInfoService } from "../nodeInfoService";
import { NovaTimeService } from "../novaTimeService";

type EpochUpdate = ITimedEntry & {
epochIndex: number;
Expand All @@ -84,6 +83,12 @@ type EpochUpdate = ITimedEntry & {
* Epoch analyitics cache MAX size.
*/
const EPOCH_CACHE_MAX = 20;

/**
* Epoch analyitics cache MAX size.
*/
const MANA_BURNED_CACHE_MAX = 20;

/**
* The collect graph data interval cron expression.
* Every hour at 59 min 55 sec
Expand Down Expand Up @@ -123,15 +128,27 @@ export class InfluxServiceNova extends InfluxDbClient {
*/
protected readonly _analyticsCache: IInfluxAnalyticsCache;

/**
* The current influx mana burned in slot cache.
*/
protected readonly _manaBurnedInSlotCache: ManaBurnedInSlotCache;

/**
* The network in context for this client.
*/
protected readonly _network: INetwork;

/**
* Nova time service for conversions.
*/
private readonly _novatimeService: NovaTimeService;

constructor(network: INetwork) {
super(network);
this._novatimeService = ServiceFactory.get<NovaTimeService>(`nova-time-${network.network}`);
this._dailyCache = initializeEmptyDailyCache();
this._epochCache = new Map();
this._manaBurnedInSlotCache = new Map();
this._analyticsCache = {};
}

Expand Down Expand Up @@ -243,13 +260,45 @@ export class InfluxServiceNova extends InfluxDbClient {
return this._analyticsCache.delegatorsCount;
}

public getEpochAnalyticStats(epochIndex: number): IEpochAnalyticStats | undefined {
public async getEpochAnalyticStats(epochIndex: number) {
if (!this._epochCache.get(epochIndex)) {
await this.collectEpochStatsByIndex(epochIndex);
}

return this._epochCache.get(epochIndex);
}

public async fetchAnalyticsForEpoch(epochIndex: number, parameters: ProtocolParameters) {
await this.collectEpochStatsByIndex(epochIndex, parameters);
return this._epochCache.get(epochIndex);
/**
* Get the manaBurned stats by slot index.
* @param slotIndex - The slot index.
*/
public async getManaBurnedBySlotIndex(slotIndex: number): Promise<ManaBurnedInSlot | null> {
if (this._manaBurnedInSlotCache.has(slotIndex)) {
return this._manaBurnedInSlotCache.get(slotIndex);
}

const { from, to } = this._novatimeService.getSlotIndexToUnixTimeRange(slotIndex);
const fromNano = toNanoDate((moment(Number(from) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());
const toNano = toNanoDate((moment(Number(to) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());

let manaBurnedResult: ManaBurnedInSlot | null = null;

await this.queryInflux<ManaBurnedInSlot>(MANA_BURN_DAILY_QUERY.partial, fromNano, toNano)
.then((results) => {
for (const update of results) {
if (update.manaBurned !== undefined) {
update.slotIndex = slotIndex;
this.updateBurnedManaCache(update);

manaBurnedResult = update;
}
}
})
.catch((e) => {
logger.warn(`[InfluxClient] Query 'mana burned in slot' failed for (${this._network.network}). Cause ${e}`);
});

return manaBurnedResult;
}

protected setupDataCollection() {
Expand Down Expand Up @@ -429,12 +478,10 @@ export class InfluxServiceNova extends InfluxDbClient {
/**
* Get the epoch analytics by index and set it in the cache.
* @param epochIndex - The epoch index.
* @param parameters - The protocol parameters information.
*/
private async collectEpochStatsByIndex(epochIndex: number, parameters: ProtocolParameters) {
private async collectEpochStatsByIndex(epochIndex: number) {
try {
const epochIndexToUnixTimeRange = epochIndexToUnixTimeRangeConverter(parameters);
const { from, to } = epochIndexToUnixTimeRange(epochIndex);
const { from, to } = this._novatimeService.getEpochIndexToUnixTimeRange(epochIndex);
const fromNano = toNanoDate((moment(Number(from) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());
const toNano = toNanoDate((moment(Number(to) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());

Expand All @@ -461,12 +508,9 @@ export class InfluxServiceNova extends InfluxDbClient {
private async collectEpochStats() {
try {
logger.debug(`[InfluxNova] Collecting epoch stats for "${this._network.network}"`);
const nodeService = ServiceFactory.get<NodeInfoService>(`node-info-${this._network.network}`);
const parameters = await nodeService.getProtocolParameters();
const unixTimestampToEpochIndex = unixTimestampToEpochIndexConverter(parameters);
const epochIndex = unixTimestampToEpochIndex(moment().unix());
const epochIndex = this._novatimeService.getUnixTimestampToEpochIndex(moment().unix());
// eslint-disable-next-line no-void
void this.collectEpochStatsByIndex(epochIndex, parameters);
void this.collectEpochStatsByIndex(epochIndex);
} catch (err) {
logger.warn(`[InfluxNova] Failed refreshing epoch stats for "${this._network.network}". Cause: ${err}`);
}
Expand Down Expand Up @@ -496,7 +540,34 @@ export class InfluxServiceNova extends InfluxDbClient {
lowestIndex = index;
}

if (epochIndex < lowestIndex) {
if (index < lowestIndex) {
lowestIndex = index;
}
}

logger.debug(`[InfluxNova] Deleting epoch index "${lowestIndex}" ("${this._network.network}")`);

this._epochCache.delete(lowestIndex);
}
}
}

private updateBurnedManaCache(update: ManaBurnedInSlot) {
if (update.slotIndex && !this._manaBurnedInSlotCache.has(update.slotIndex)) {
const { slotIndex } = update;

this._manaBurnedInSlotCache.set(slotIndex, update);

logger.debug(`[InfluxNova] Added slot index "${slotIndex}" to ManaBurned cache for "${this._network.network}"`);

if (this._manaBurnedInSlotCache.size > MANA_BURNED_CACHE_MAX) {
let lowestIndex: number;
for (const index of this._manaBurnedInSlotCache.keys()) {
if (!lowestIndex) {
lowestIndex = index;
}

if (index < lowestIndex) {
lowestIndex = index;
}
}
Expand Down
Loading

0 comments on commit 564ea92

Please sign in to comment.