From adc205028e21ebe20b438c6950cc14ed2ab84ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bego=C3=B1a=20=C3=81lvarez=20de=20la=20Cruz?= Date: Tue, 30 Jan 2024 13:33:14 +0100 Subject: [PATCH] refactor: nova api service (#1023) * refactor: nova api service * fix: nova feed --- api/src/initServices.ts | 37 +++++-- api/src/routes/nova/account/get.ts | 6 +- api/src/routes/nova/block/get.ts | 5 +- api/src/routes/nova/block/metadata/get.ts | 5 +- api/src/routes/nova/output/get.ts | 5 +- api/src/services/nova/feed/novaFeed.ts | 33 +++--- api/src/services/nova/novaApi.ts | 122 ---------------------- api/src/services/nova/novaApiService.ts | 111 ++++++++++++++++++++ 8 files changed, 169 insertions(+), 155 deletions(-) delete mode 100644 api/src/services/nova/novaApi.ts create mode 100644 api/src/services/nova/novaApiService.ts diff --git a/api/src/initServices.ts b/api/src/initServices.ts index d54b0e1fb..0873bba40 100644 --- a/api/src/initServices.ts +++ b/api/src/initServices.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-unresolved */ import { MqttClient as ChrysalisMqttClient } from "@iota/mqtt.js"; -import { IClientOptions, Client as StardustClient } from "@iota/sdk"; -import { Client as NovaClient } from "@iota/sdk-nova"; +import { IClientOptions as IStardustClientOptions, Client as StardustClient } from "@iota/sdk"; +import { IClientOptions as INovaClientOptions, Client as NovaClient } from "@iota/sdk-nova"; import { ServiceFactory } from "./factories/serviceFactory"; import logger from "./logger"; import { IConfiguration } from "./models/configuration/IConfiguration"; @@ -24,6 +24,7 @@ import { LocalStorageService } from "./services/localStorageService"; import { NetworkService } from "./services/networkService"; import { NovaFeed } from "./services/nova/feed/novaFeed"; import { NodeInfoService as NodeInfoServiceNova } from "./services/nova/nodeInfoService"; +import { NovaApiService } from "./services/nova/novaApiService"; import { ChronicleService } from "./services/stardust/chronicleService"; import { StardustFeed } from "./services/stardust/feed/stardustFeed"; import { InfluxDBService } from "./services/stardust/influx/influxDbService"; @@ -158,12 +159,14 @@ function initChrysalisServices(networkConfig: INetwork): void { function initStardustServices(networkConfig: INetwork): void { logger.verbose(`Initializing Stardust services for ${networkConfig.network}`); - const stardustClientParams: IClientOptions = { + const stardustClientParams: IStardustClientOptions = { primaryNode: networkConfig.provider, }; if (networkConfig.permaNodeEndpoint) { stardustClientParams.nodes = [networkConfig.permaNodeEndpoint]; + // Client with permanode needs the ignoreNodeHealth as chronicle is considered "not healthy" by the sdk + // Related: https://github.com/iotaledger/inx-chronicle/issues/1302 stardustClientParams.ignoreNodeHealth = true; const chronicleService = new ChronicleService(networkConfig); @@ -206,21 +209,33 @@ function initStardustServices(networkConfig: INetwork): void { function initNovaServices(networkConfig: INetwork): void { logger.verbose(`Initializing Nova services for ${networkConfig.network}`); + const novaClientParams: INovaClientOptions = { + primaryNode: networkConfig.provider, + }; + + if (networkConfig.permaNodeEndpoint) { + novaClientParams.nodes = [networkConfig.permaNodeEndpoint]; + // Client with permanode needs the ignoreNodeHealth as chronicle is considered "not healthy" by the sdk + // Related: https://github.com/iotaledger/inx-chronicle/issues/1302 + novaClientParams.ignoreNodeHealth = true; + + const chronicleService = new ChronicleService(networkConfig); + ServiceFactory.register(`chronicle-${networkConfig.network}`, () => chronicleService); + } + // eslint-disable-next-line no-void - void NovaClient.create({ - nodes: [networkConfig.provider], - brokerOptions: { useWs: true }, - // Needed only for now in local development (NOT FOR PROD) - ignoreNodeHealth: true, - }).then((novaClient) => { + void NovaClient.create(novaClientParams).then((novaClient) => { ServiceFactory.register(`client-${networkConfig.network}`, () => novaClient); + const novaApiService = new NovaApiService(networkConfig); + ServiceFactory.register(`api-service-${networkConfig.network}`, () => novaApiService); + // eslint-disable-next-line no-void void NodeInfoServiceNova.build(networkConfig).then((nodeInfoService) => { ServiceFactory.register(`node-info-${networkConfig.network}`, () => nodeInfoService); - const feedInstance = new NovaFeed(networkConfig.network); - ServiceFactory.register(`feed-${networkConfig.network}`, () => feedInstance); + const novaFeed = new NovaFeed(networkConfig); + ServiceFactory.register(`feed-${networkConfig.network}`, () => novaFeed); }); }); } diff --git a/api/src/routes/nova/account/get.ts b/api/src/routes/nova/account/get.ts index a8a9c82fb..06f690f33 100644 --- a/api/src/routes/nova/account/get.ts +++ b/api/src/routes/nova/account/get.ts @@ -4,7 +4,7 @@ import { IAccountResponse } from "../../../models/api/nova/IAccountResponse"; import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { NOVA } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { NovaApi } from "../../../services/nova/novaApi"; +import { NovaApiService } from "../../../services/nova/novaApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -24,6 +24,6 @@ export async function get(config: IConfiguration, request: IAccountRequest): Pro if (networkConfig.protocolVersion !== NOVA) { return {}; } - - return NovaApi.accountDetails(networkConfig, request.accountId); + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.accountDetails(request.accountId); } diff --git a/api/src/routes/nova/block/get.ts b/api/src/routes/nova/block/get.ts index 8fb82a0d3..36d780c8e 100644 --- a/api/src/routes/nova/block/get.ts +++ b/api/src/routes/nova/block/get.ts @@ -4,7 +4,7 @@ import { IBlockRequest } from "../../../models/api/stardust/IBlockRequest"; import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { NOVA } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { NovaApi } from "../../../services/nova/novaApi"; +import { NovaApiService } from "../../../services/nova/novaApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(_: IConfiguration, request: IBlockRequest): Promise(`api-service-${networkConfig.network}`); + return novaApiService.block(request.blockId); } diff --git a/api/src/routes/nova/block/metadata/get.ts b/api/src/routes/nova/block/metadata/get.ts index 487aec349..78642ae26 100644 --- a/api/src/routes/nova/block/metadata/get.ts +++ b/api/src/routes/nova/block/metadata/get.ts @@ -4,7 +4,7 @@ import { IBlockRequest } from "../../../../models/api/stardust/IBlockRequest"; import { IConfiguration } from "../../../../models/configuration/IConfiguration"; import { NOVA } from "../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../services/networkService"; -import { NovaApi } from "../../../../services/nova/novaApi"; +import { NovaApiService } from "../../../../services/nova/novaApiService"; import { ValidationHelper } from "../../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(_: IConfiguration, request: IBlockRequest): Promise(`api-service-${networkConfig.network}`); + return novaApiService.blockDetails(request.blockId); } diff --git a/api/src/routes/nova/output/get.ts b/api/src/routes/nova/output/get.ts index f098c21cc..20f08165d 100644 --- a/api/src/routes/nova/output/get.ts +++ b/api/src/routes/nova/output/get.ts @@ -4,7 +4,7 @@ import { IOutputDetailsResponse } from "../../../models/api/nova/IOutputDetailsR import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { NOVA } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { NovaApi } from "../../../services/nova/novaApi"; +import { NovaApiService } from "../../../services/nova/novaApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(config: IConfiguration, request: IOutputDetailsRequest return {}; } - return NovaApi.outputDetails(networkConfig, request.outputId); + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.outputDetails(request.outputId); } diff --git a/api/src/services/nova/feed/novaFeed.ts b/api/src/services/nova/feed/novaFeed.ts index e563e0772..765fe6614 100644 --- a/api/src/services/nova/feed/novaFeed.ts +++ b/api/src/services/nova/feed/novaFeed.ts @@ -5,6 +5,7 @@ import { ClassConstructor, plainToInstance } from "class-transformer"; import { ServiceFactory } from "../../../factories/serviceFactory"; import logger from "../../../logger"; import { IFeedUpdate } from "../../../models/api/nova/feed/IFeedUpdate"; +import { INetwork } from "../../../models/db/INetwork"; import { NodeInfoService } from "../nodeInfoService"; /** @@ -22,29 +23,35 @@ export class NovaFeed { /** * Mqtt service for data (upstream). */ - private readonly _mqttClient: Client; + private _mqttClient: Client; /** * The network in context. */ - private readonly network: string; + private readonly networkId: string; /** * Creates a new instance of NovaFeed. - * @param networkId The network id. + * @param network The network config. */ - constructor(networkId: string) { + constructor(network: INetwork) { logger.debug("[NovaFeed] Constructing a Nova Feed"); this.blockSubscribers = {}; - this.network = networkId; - this._mqttClient = ServiceFactory.get(`client-${networkId}`); - const nodeInfoService = ServiceFactory.get(`node-info-${networkId}`); + this.networkId = network.network; - if (this._mqttClient && nodeInfoService) { - this.connect(); - } else { - throw new Error(`Failed to build novaFeed instance for ${networkId}`); - } + // eslint-disable-next-line no-void + void Client.create({ + nodes: [network.provider], + brokerOptions: { useWs: true }, + }).then((_mqttClient) => { + this._mqttClient = _mqttClient; + const nodeInfoService = ServiceFactory.get(`node-info-${this.networkId}`); + if (this._mqttClient && nodeInfoService) { + this.connect(); + } else { + throw new Error(`Failed to build novaFeed instance for ${this.networkId}`); + } + }); } /** @@ -61,7 +68,7 @@ export class NovaFeed { * @param subscriptionId The id to unsubscribe. */ public unsubscribeBlocks(subscriptionId: string): void { - logger.debug(`[NovaFeed] Removing subscriber ${subscriptionId} from blocks (${this.network})`); + logger.debug(`[NovaFeed] Removing subscriber ${subscriptionId} from blocks (${this.networkId})`); delete this.blockSubscribers[subscriptionId]; } diff --git a/api/src/services/nova/novaApi.ts b/api/src/services/nova/novaApi.ts deleted file mode 100644 index 53ee46fb9..000000000 --- a/api/src/services/nova/novaApi.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* eslint-disable import/no-unresolved */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { __ClientMethods__, OutputResponse, Client, Block, IBlockMetadata } 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 { INetwork } from "../../models/db/INetwork"; -import { HexHelper } from "../../utils/hexHelper"; - -type NameType = T extends { name: infer U } ? U : never; -type ExtractedMethodNames = NameType<__ClientMethods__>; - -/** - * Class to interact with the nova API. - */ -export class NovaApi { - /** - * Get a block. - * @param network The network to find the items on. - * @param blockId The block id to get the details. - * @returns The block response. - */ - public static async block(network: INetwork, blockId: string): Promise { - blockId = HexHelper.addPrefix(blockId); - const block = await this.tryFetchNodeThenPermanode(blockId, "getBlock", network); - - if (!block) { - return { error: `Couldn't find block with id ${blockId}` }; - } - - try { - if (block && Object.keys(block).length > 0) { - return { - block, - }; - } - } catch (e) { - logger.error(`Failed fetching block with block id ${blockId}. Cause: ${e}`); - return { error: "Block fetch failed." }; - } - } - - /** - * Get the block details. - * @param network The network to find the items on. - * @param blockId The block id to get the details. - * @returns The item details. - */ - public static async blockDetails(network: INetwork, blockId: string): Promise { - blockId = HexHelper.addPrefix(blockId); - const metadata = await this.tryFetchNodeThenPermanode(blockId, "getBlockMetadata", network); - - if (metadata) { - return { - metadata, - }; - } - } - - /** - * Get the output details. - * @param network The network to find the items on. - * @param outputId The output id to get the details. - * @returns The item details. - */ - public static async outputDetails(network: INetwork, outputId: string): Promise { - const outputResponse = await this.tryFetchNodeThenPermanode(outputId, "getOutput", network); - - return outputResponse ? { output: outputResponse } : { message: "Output not found" }; - } - - /** - * Get the account details. - * @param network The network to find the items on. - * @param accountId The accountId to get the details for. - * @returns The account details. - */ - public static async accountDetails(network: INetwork, accountId: string): Promise { - const accountOutputId = await this.tryFetchNodeThenPermanode(accountId, "accountOutputId", network); - - if (accountOutputId) { - const outputResponse = await this.outputDetails(network, accountOutputId); - return outputResponse.error ? { error: outputResponse.error } : { accountDetails: outputResponse.output }; - } - - return { message: "Account output not found" }; - } - - /** - * Generic helper function to try fetching from node client. - * On failure (or not present), we try to fetch from permanode (if configured). - * @param args The argument(s) to pass to the fetch calls. - * @param methodName The function to call on the client. - * @param network The network config in context. - * @returns The results or null if call(s) failed. - */ - public static async tryFetchNodeThenPermanode(args: A, methodName: ExtractedMethodNames, network: INetwork): Promise | null { - const { permaNodeEndpoint, disableApiFallback } = network; - const isFallbackEnabled = !disableApiFallback; - const client = ServiceFactory.get(`client-${network.network}`); - - try { - // try fetch from node - const result: Promise = client[methodName](args); - return await result; - } catch {} - - if (permaNodeEndpoint && isFallbackEnabled) { - const permanodeClient = ServiceFactory.get(`permanode-client-${network.network}`); - try { - // try fetch from permanode (chronicle) - const result: Promise = permanodeClient[methodName](args); - return await result; - } catch {} - } - - return null; - } -} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts new file mode 100644 index 000000000..9ae7ccb6e --- /dev/null +++ b/api/src/services/nova/novaApiService.ts @@ -0,0 +1,111 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { Client } 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 { INetwork } from "../../models/db/INetwork"; +import { HexHelper } from "../../utils/hexHelper"; + +/** + * Class to interact with the nova API. + */ +export class NovaApiService { + /** + * The network in context. + */ + private readonly network: INetwork; + + /** + * The client to use for requests. + */ + private readonly client: Client; + + constructor(network: INetwork) { + this.network = network; + this.client = ServiceFactory.get(`client-${network.network}`); + } + + /** + * Get a block. + * @param blockId The block id to get the details. + * @returns The block response. + */ + public async block(blockId: string): Promise { + blockId = HexHelper.addPrefix(blockId); + try { + const block = await this.client.getBlock(blockId); + + if (!block) { + return { error: `Couldn't find block with id ${blockId}` }; + } + + if (block && Object.keys(block).length > 0) { + return { + block, + }; + } + } catch (e) { + logger.error(`Failed fetching block with block id ${blockId}. Cause: ${e}`); + return { error: "Block fetch failed." }; + } + } + + /** + * Get the block details. + * @param blockId The block id to get the details. + * @returns The item details. + */ + public async blockDetails(blockId: string): Promise { + try { + blockId = HexHelper.addPrefix(blockId); + const metadata = await this.client.getBlockMetadata(blockId); + + if (metadata) { + return { + metadata, + }; + } + } catch (e) { + logger.error(`Failed fetching block metadata with block id ${blockId}. Cause: ${e}`); + return { error: "Block metadata fetch failed." }; + } + } + + /** + * Get the output details. + * @param outputId The output id to get the details. + * @returns The item details. + */ + public async outputDetails(outputId: string): Promise { + try { + const outputResponse = await this.client.getOutput(outputId); + return { output: outputResponse }; + } catch (e) { + logger.error(`Failed fetching output with output id ${outputId}. Cause: ${e}`); + return { error: "Output not found" }; + } + } + + /** + * Get the account details. + * @param accountId The accountId to get the details for. + * @returns The account details. + */ + public async accountDetails(accountId: string): Promise { + try { + const accountOutputId = await this.client.accountOutputId(accountId); + + if (accountOutputId) { + const outputResponse = await this.outputDetails(accountOutputId); + + return outputResponse.error ? { error: outputResponse.error } : { accountDetails: outputResponse.output }; + } + } catch { + return { message: "Account output not found" }; + } + } +}