From cc23cfb01d2d137ca291c7a4b53590d256407ae6 Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Mon, 22 Jan 2024 14:01:05 +0100 Subject: [PATCH] fix: use both node and permanode for client data fetch (#984) * fix: use both node and permanode for client data fetch * feat: create mqttClient inside the StardustFeed class * chore: rename `tryFetchNodeThenPermanode` function * fix: wrap functions in try catch in `stardustTangleHelper` * chore: rename `stardustTangleHelper` to `stardustApiService` * fix: rename tangleHelper --- api/src/initServices.ts | 29 +- .../routes/stardust/address/balance/get.ts | 5 +- .../stardust/address/outputs/alias/get.ts | 5 +- .../stardust/address/outputs/basic/get.ts | 5 +- .../stardust/address/outputs/nft/get.ts | 5 +- .../routes/stardust/alias/foundries/get.ts | 5 +- api/src/routes/stardust/alias/get.ts | 5 +- api/src/routes/stardust/block/get.ts | 5 +- api/src/routes/stardust/block/metadata/get.ts | 5 +- api/src/routes/stardust/foundry/get.ts | 5 +- api/src/routes/stardust/milestone/get.ts | 5 +- api/src/routes/stardust/nft/get.ts | 5 +- api/src/routes/stardust/output/get.ts | 5 +- api/src/routes/stardust/output/tagged/get.ts | 7 +- .../stardust/participation/events/get.ts | 5 +- api/src/routes/stardust/search.ts | 5 +- api/src/routes/stardust/transaction/get.ts | 5 +- .../services/stardust/feed/stardustFeed.ts | 23 +- .../services/stardust/stardustApiService.ts | 515 ++++++++++++++++ api/src/utils/stardust/searchExecutor.ts | 47 +- .../utils/stardust/stardustTangleHelper.ts | 569 ------------------ 21 files changed, 619 insertions(+), 646 deletions(-) create mode 100644 api/src/services/stardust/stardustApiService.ts delete mode 100644 api/src/utils/stardust/stardustTangleHelper.ts diff --git a/api/src/initServices.ts b/api/src/initServices.ts index 4c643f32e..921371b39 100644 --- a/api/src/initServices.ts +++ b/api/src/initServices.ts @@ -1,5 +1,5 @@ import { MqttClient as ChrysalisMqttClient } from "@iota/mqtt.js"; -import { Client as StardustClient } from "@iota/sdk"; +import { IClientOptions, Client as StardustClient } from "@iota/sdk"; import { ServiceFactory } from "./factories/serviceFactory"; import logger from "./logger"; import { IConfiguration } from "./models/configuration/IConfiguration"; @@ -24,6 +24,7 @@ import { ChronicleService } from "./services/stardust/chronicleService"; import { StardustFeed } from "./services/stardust/feed/stardustFeed"; import { InfluxDBService } from "./services/stardust/influx/influxDbService"; import { NodeInfoService } from "./services/stardust/nodeInfoService"; +import { StardustApiService } from "./services/stardust/stardustApiService"; import { StardustStatsService } from "./services/stardust/stats/stardustStatsService"; const CURRENCY_UPDATE_INTERVAL_MS = 5 * 60000; @@ -141,30 +142,30 @@ function initChrysalisServices(networkConfig: INetwork): void { */ function initStardustServices(networkConfig: INetwork): void { logger.verbose(`Initializing Stardust services for ${networkConfig.network}`); - const stardustClient = new StardustClient({ - nodes: [networkConfig.provider], - brokerOptions: { useWs: true }, - }); - ServiceFactory.register(`client-${networkConfig.network}`, () => stardustClient); + + const stardustClientParams: IClientOptions = { + primaryNode: networkConfig.provider, + }; if (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 - const stardustPermanodeClient = new StardustClient({ - nodes: [networkConfig.permaNodeEndpoint], - ignoreNodeHealth: true, - }); - ServiceFactory.register(`permanode-client-${networkConfig.network}`, () => stardustPermanodeClient); + stardustClientParams.nodes = [networkConfig.permaNodeEndpoint]; + stardustClientParams.ignoreNodeHealth = true; const chronicleService = new ChronicleService(networkConfig); ServiceFactory.register(`chronicle-${networkConfig.network}`, () => chronicleService); } + const stardustClient = new StardustClient(stardustClientParams); + ServiceFactory.register(`client-${networkConfig.network}`, () => stardustClient); + + const stardustApiService = new StardustApiService(networkConfig); + ServiceFactory.register(`api-service-${networkConfig.network}`, () => stardustApiService); + // eslint-disable-next-line no-void void NodeInfoService.build(networkConfig).then((nodeInfoService) => { ServiceFactory.register(`node-info-${networkConfig.network}`, () => nodeInfoService); - const stardustFeed = new StardustFeed(networkConfig.network); + const stardustFeed = new StardustFeed(networkConfig); ServiceFactory.register(`feed-${networkConfig.network}`, () => stardustFeed); }); diff --git a/api/src/routes/stardust/address/balance/get.ts b/api/src/routes/stardust/address/balance/get.ts index 2d0032c9b..74110051e 100644 --- a/api/src/routes/stardust/address/balance/get.ts +++ b/api/src/routes/stardust/address/balance/get.ts @@ -4,7 +4,7 @@ import IAddressDetailsWithBalance from "../../../../models/api/stardust/IAddress import { IConfiguration } from "../../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../services/networkService"; -import { StardustTangleHelper } from "../../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../../utils/validationHelper"; /** @@ -24,5 +24,6 @@ export async function get(config: IConfiguration, request: IAddressBalanceReques return undefined; } - return StardustTangleHelper.addressDetails(networkConfig, request.address); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.addressDetails(request.address); } diff --git a/api/src/routes/stardust/address/outputs/alias/get.ts b/api/src/routes/stardust/address/outputs/alias/get.ts index 6fcc1aec1..aeb9aa652 100644 --- a/api/src/routes/stardust/address/outputs/alias/get.ts +++ b/api/src/routes/stardust/address/outputs/alias/get.ts @@ -4,7 +4,7 @@ import { IAddressDetailsResponse } from "../../../../../models/api/stardust/IAdd import { IConfiguration } from "../../../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../../services/networkService"; -import { StardustTangleHelper } from "../../../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../../../utils/validationHelper"; /** @@ -24,5 +24,6 @@ export async function get(config: IConfiguration, request: IAddressDetailsReques return {}; } - return StardustTangleHelper.aliasOutputDetailsByAddress(networkConfig, request.address); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.aliasOutputDetailsByAddress(request.address); } diff --git a/api/src/routes/stardust/address/outputs/basic/get.ts b/api/src/routes/stardust/address/outputs/basic/get.ts index 616fc181b..69565d796 100644 --- a/api/src/routes/stardust/address/outputs/basic/get.ts +++ b/api/src/routes/stardust/address/outputs/basic/get.ts @@ -4,7 +4,7 @@ import { IAddressDetailsResponse } from "../../../../../models/api/stardust/IAdd import { IConfiguration } from "../../../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../../services/networkService"; -import { StardustTangleHelper } from "../../../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../../../utils/validationHelper"; /** @@ -24,5 +24,6 @@ export async function get(config: IConfiguration, request: IAddressDetailsReques return {}; } - return StardustTangleHelper.basicOutputDetailsByAddress(networkConfig, request.address); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.basicOutputDetailsByAddress(request.address); } diff --git a/api/src/routes/stardust/address/outputs/nft/get.ts b/api/src/routes/stardust/address/outputs/nft/get.ts index 85b8cd4bd..f738f656d 100644 --- a/api/src/routes/stardust/address/outputs/nft/get.ts +++ b/api/src/routes/stardust/address/outputs/nft/get.ts @@ -4,7 +4,7 @@ import { IAddressDetailsResponse } from "../../../../../models/api/stardust/IAdd import { IConfiguration } from "../../../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../../services/networkService"; -import { StardustTangleHelper } from "../../../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../../../utils/validationHelper"; /** @@ -24,5 +24,6 @@ export async function get(config: IConfiguration, request: IAddressDetailsReques return {}; } - return StardustTangleHelper.nftOutputDetailsByAddress(networkConfig, request.address); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.nftOutputDetailsByAddress(request.address); } diff --git a/api/src/routes/stardust/alias/foundries/get.ts b/api/src/routes/stardust/alias/foundries/get.ts index 548673d73..3417d25d0 100644 --- a/api/src/routes/stardust/alias/foundries/get.ts +++ b/api/src/routes/stardust/alias/foundries/get.ts @@ -4,7 +4,7 @@ import { IFoundriesResponse } from "../../../../models/api/stardust/foundry/IFou import { IConfiguration } from "../../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../services/networkService"; -import { StardustTangleHelper } from "../../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(config: IConfiguration, request: IFoundriesRequest): P return {}; } - return StardustTangleHelper.aliasFoundries(networkConfig, request.aliasAddress); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.aliasFoundries(request.aliasAddress); } diff --git a/api/src/routes/stardust/alias/get.ts b/api/src/routes/stardust/alias/get.ts index 41a2e6bc2..f2ab84a87 100644 --- a/api/src/routes/stardust/alias/get.ts +++ b/api/src/routes/stardust/alias/get.ts @@ -4,7 +4,7 @@ import { IAliasResponse } from "../../../models/api/stardust/IAliasResponse"; import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { StardustTangleHelper } from "../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(config: IConfiguration, request: IAliasRequest): Promi return {}; } - return StardustTangleHelper.aliasDetails(networkConfig, request.aliasId); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.aliasDetails(request.aliasId); } diff --git a/api/src/routes/stardust/block/get.ts b/api/src/routes/stardust/block/get.ts index 49ecc7457..d5b912976 100644 --- a/api/src/routes/stardust/block/get.ts +++ b/api/src/routes/stardust/block/get.ts @@ -4,7 +4,7 @@ import { IBlockResponse } from "../../../models/api/stardust/IBlockResponse"; import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { StardustTangleHelper } from "../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(_: IConfiguration, request: IBlockRequest): Promise(`api-service-${networkConfig.network}`); + return stardustApiService.block(request.blockId); } diff --git a/api/src/routes/stardust/block/metadata/get.ts b/api/src/routes/stardust/block/metadata/get.ts index efdcfeea0..6efb937e6 100644 --- a/api/src/routes/stardust/block/metadata/get.ts +++ b/api/src/routes/stardust/block/metadata/get.ts @@ -4,7 +4,7 @@ import { IBlockRequest } from "../../../../models/api/stardust/IBlockRequest"; import { IConfiguration } from "../../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../services/networkService"; -import { StardustTangleHelper } from "../../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(_: IConfiguration, request: IBlockRequest): Promise(`api-service-${networkConfig.network}`); + return stardustApiService.blockDetails(request.blockId); } diff --git a/api/src/routes/stardust/foundry/get.ts b/api/src/routes/stardust/foundry/get.ts index d4d7836ed..e5233dd83 100644 --- a/api/src/routes/stardust/foundry/get.ts +++ b/api/src/routes/stardust/foundry/get.ts @@ -4,7 +4,7 @@ import { IFoundryResponse } from "../../../models/api/stardust/foundry/IFoundryR import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { StardustTangleHelper } from "../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(config: IConfiguration, request: IFoundryRequest): Pro return {}; } - return StardustTangleHelper.foundryDetails(networkConfig, request.foundryId); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.foundryDetails(request.foundryId); } diff --git a/api/src/routes/stardust/milestone/get.ts b/api/src/routes/stardust/milestone/get.ts index bc71bb4be..9d3c52992 100644 --- a/api/src/routes/stardust/milestone/get.ts +++ b/api/src/routes/stardust/milestone/get.ts @@ -4,7 +4,7 @@ import { IMilestoneDetailsResponse } from "../../../models/api/stardust/mileston import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { StardustTangleHelper } from "../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,7 +25,8 @@ export async function get(config: IConfiguration, request: IMilestoneDetailsRequ return {}; } - const milestoneDetails = await StardustTangleHelper.milestoneDetailsByIndex(networkConfig, Number(request.milestoneIndex)); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + const milestoneDetails = await stardustApiService.milestoneDetailsByIndex(Number(request.milestoneIndex)); return milestoneDetails; } diff --git a/api/src/routes/stardust/nft/get.ts b/api/src/routes/stardust/nft/get.ts index 32285bde8..88b04bc3a 100644 --- a/api/src/routes/stardust/nft/get.ts +++ b/api/src/routes/stardust/nft/get.ts @@ -4,7 +4,7 @@ import { INftDetailsResponse } from "../../../models/api/stardust/nft/INftDetail import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { StardustTangleHelper } from "../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(config: IConfiguration, request: INftDetailsRequest): return {}; } - return StardustTangleHelper.nftDetails(networkConfig, request.nftId); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.nftDetails(request.nftId); } diff --git a/api/src/routes/stardust/output/get.ts b/api/src/routes/stardust/output/get.ts index 720dea6f5..413d87c20 100644 --- a/api/src/routes/stardust/output/get.ts +++ b/api/src/routes/stardust/output/get.ts @@ -4,7 +4,7 @@ import { IOutputDetailsResponse } from "../../../models/api/stardust/IOutputDeta import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { StardustTangleHelper } from "../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(config: IConfiguration, request: IOutputDetailsRequest return {}; } - return StardustTangleHelper.outputDetails(networkConfig, request.outputId); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.outputDetails(request.outputId); } diff --git a/api/src/routes/stardust/output/tagged/get.ts b/api/src/routes/stardust/output/tagged/get.ts index c96ad74d9..cf96cc4f3 100644 --- a/api/src/routes/stardust/output/tagged/get.ts +++ b/api/src/routes/stardust/output/tagged/get.ts @@ -5,8 +5,8 @@ import { INftOutputsResponse } from "../../../../models/api/stardust/nft/INftOut import { IConfiguration } from "../../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../services/networkService"; +import { StardustApiService } from "../../../../services/stardust/stardustApiService"; import { Converter } from "../../../../utils/convertUtils"; -import { StardustTangleHelper } from "../../../../utils/stardust/stardustTangleHelper"; import { ValidationHelper } from "../../../../utils/validationHelper"; /** @@ -29,11 +29,12 @@ export async function get(_: IConfiguration, request: ITaggedOutputsRequest): Pr } const tagHex = Converter.utf8ToHex(request.tag, true); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); if (request.outputType === "basic") { - return StardustTangleHelper.taggedBasicOutputs(networkConfig, tagHex, 10, request.cursor); + return stardustApiService.taggedBasicOutputs(tagHex, 10, request.cursor); } else if (request.outputType === "nft") { - return StardustTangleHelper.taggedNftOutputs(networkConfig, tagHex, 10, request.cursor); + return stardustApiService.taggedNftOutputs(tagHex, 10, request.cursor); } return { error: "Unsupported output type" }; diff --git a/api/src/routes/stardust/participation/events/get.ts b/api/src/routes/stardust/participation/events/get.ts index de54dfa68..8de22a1bb 100644 --- a/api/src/routes/stardust/participation/events/get.ts +++ b/api/src/routes/stardust/participation/events/get.ts @@ -4,7 +4,7 @@ import { IParticipationEventResponse } from "../../../../models/api/stardust/par import { IConfiguration } from "../../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../../models/db/protocolVersion"; import { NetworkService } from "../../../../services/networkService"; -import { StardustTangleHelper } from "../../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../../utils/validationHelper"; /** @@ -24,5 +24,6 @@ export async function get(config: IConfiguration, request: IParticipationEventRe return {}; } - return StardustTangleHelper.participationEventDetails(networkConfig, request.eventId); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.participationEventDetails(request.eventId); } diff --git a/api/src/routes/stardust/search.ts b/api/src/routes/stardust/search.ts index 44ead06d8..e19a88113 100644 --- a/api/src/routes/stardust/search.ts +++ b/api/src/routes/stardust/search.ts @@ -4,7 +4,7 @@ import { ISearchResponse } from "../../models/api/stardust/ISearchResponse"; import { IConfiguration } from "../../models/configuration/IConfiguration"; import { STARDUST } from "../../models/db/protocolVersion"; import { NetworkService } from "../../services/networkService"; -import { StardustTangleHelper } from "../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function search(_: IConfiguration, request: ISearchRequest): Promis return {}; } - return StardustTangleHelper.search(networkConfig, request.query); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.search(request.query); } diff --git a/api/src/routes/stardust/transaction/get.ts b/api/src/routes/stardust/transaction/get.ts index d95dbbd4f..a559badeb 100644 --- a/api/src/routes/stardust/transaction/get.ts +++ b/api/src/routes/stardust/transaction/get.ts @@ -4,7 +4,7 @@ import { ITransactionDetailsResponse } from "../../../models/api/stardust/ITrans import { IConfiguration } from "../../../models/configuration/IConfiguration"; import { STARDUST } from "../../../models/db/protocolVersion"; import { NetworkService } from "../../../services/networkService"; -import { StardustTangleHelper } from "../../../utils/stardust/stardustTangleHelper"; +import { StardustApiService } from "../../../services/stardust/stardustApiService"; import { ValidationHelper } from "../../../utils/validationHelper"; /** @@ -25,5 +25,6 @@ export async function get(config: IConfiguration, request: ITransactionDetailsRe return {}; } - return StardustTangleHelper.transactionIncludedBlock(networkConfig, request.transactionId); + const stardustApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return stardustApiService.transactionIncludedBlock(request.transactionId); } diff --git a/api/src/services/stardust/feed/stardustFeed.ts b/api/src/services/stardust/feed/stardustFeed.ts index b4db7d0f8..e013b3c2e 100644 --- a/api/src/services/stardust/feed/stardustFeed.ts +++ b/api/src/services/stardust/feed/stardustFeed.ts @@ -5,6 +5,7 @@ import logger from "../../../logger"; import { IFeedItemMetadata } from "../../../models/api/stardust/feed/IFeedItemMetadata"; import { IFeedUpdate } from "../../../models/api/stardust/feed/IFeedUpdate"; import { ILatestMilestone } from "../../../models/api/stardust/milestone/ILatestMilestonesResponse"; +import { INetwork } from "../../../models/db/INetwork"; import { blockIdFromMilestonePayload } from "../../../utils/stardust/utils"; import { NodeInfoService } from "../nodeInfoService"; @@ -60,20 +61,22 @@ export class StardustFeed { /** * The network in context (from Init). */ - private readonly network: string; + private readonly networkId: string; /** * Creates a new instance of StardustFeed. - * @param networkId The network id. + * @param network The network config. */ - constructor(networkId: string) { + constructor(network: INetwork) { this.blockSubscribers = {}; this.milestoneSubscribers = {}; this.blockMetadataCache = new Map(); - this.network = networkId; - this._mqttClient = ServiceFactory.get(`client-${networkId}`); - const nodeInfoService = ServiceFactory.get(`node-info-${networkId}`); - + this.networkId = network.network; + this._mqttClient = new Client({ + nodes: [network.provider], + brokerOptions: { useWs: true }, + }); + const nodeInfoService = ServiceFactory.get(`node-info-${this.networkId}`); if (this._mqttClient && nodeInfoService) { const nodeInfo = nodeInfoService.getNodeInfo(); this.networkProtocolVersion = nodeInfo.protocolVersion; @@ -81,7 +84,7 @@ export class StardustFeed { this.setupCacheTrimJob(); this.connect(); } else { - throw new Error(`Failed to build stardustFeed instance for ${networkId}`); + throw new Error(`Failed to build stardustFeed instance for ${this.networkId}`); } } @@ -116,7 +119,7 @@ export class StardustFeed { * @param subscriptionId The id to unsubscribe. */ public unsubscribeBlocks(subscriptionId: string): void { - logger.debug(`[StardustFeed] Removing subscriber ${subscriptionId} from blocks (${this.network})`); + logger.debug(`[StardustFeed] Removing subscriber ${subscriptionId} from blocks (${this.networkId})`); delete this.blockSubscribers[subscriptionId]; } @@ -125,7 +128,7 @@ export class StardustFeed { * @param subscriptionId The id to unsubscribe. */ public unsubscribeMilestones(subscriptionId: string): void { - logger.debug(`[StardustFeed] Removing subscriber ${subscriptionId} from milestones (${this.network})`); + logger.debug(`[StardustFeed] Removing subscriber ${subscriptionId} from milestones (${this.networkId})`); delete this.milestoneSubscribers[subscriptionId]; } diff --git a/api/src/services/stardust/stardustApiService.ts b/api/src/services/stardust/stardustApiService.ts new file mode 100644 index 000000000..0295e98b0 --- /dev/null +++ b/api/src/services/stardust/stardustApiService.ts @@ -0,0 +1,515 @@ +/* eslint-disable no-warning-comments */ +import { OutputResponse, Client, IOutputsResponse, HexEncodedString, Utils, NftQueryParameter } from "@iota/sdk"; +import { NodeInfoService } from "./nodeInfoService"; +import { ServiceFactory } from "../../factories/serviceFactory"; +import logger from "../../logger"; +import { IBasicOutputsResponse } from "../../models/api/stardust/basic/IBasicOutputsResponse"; +import { IFoundriesResponse } from "../../models/api/stardust/foundry/IFoundriesResponse"; +import { IFoundryResponse } from "../../models/api/stardust/foundry/IFoundryResponse"; +import { IAddressDetailsResponse } from "../../models/api/stardust/IAddressDetailsResponse"; +import IAddressDetailsWithBalance from "../../models/api/stardust/IAddressDetailsWithBalance"; +import { IAliasResponse } from "../../models/api/stardust/IAliasResponse"; +import { IBlockDetailsResponse } from "../../models/api/stardust/IBlockDetailsResponse"; +import { IBlockResponse } from "../../models/api/stardust/IBlockResponse"; +import { IOutputDetailsResponse } from "../../models/api/stardust/IOutputDetailsResponse"; +import { ISearchResponse } from "../../models/api/stardust/ISearchResponse"; +import { ITaggedOutputsResponse } from "../../models/api/stardust/ITaggedOutputsResponse"; +import { ITransactionDetailsResponse } from "../../models/api/stardust/ITransactionDetailsResponse"; +import { IMilestoneDetailsResponse } from "../../models/api/stardust/milestone/IMilestoneDetailsResponse"; +import { INftDetailsResponse } from "../../models/api/stardust/nft/INftDetailsResponse"; +import { INftOutputsResponse } from "../../models/api/stardust/nft/INftOutputsResponse"; +import { IParticipationEventInfo } from "../../models/api/stardust/participation/IParticipationEventInfo"; +import { IParticipationEventResponse } from "../../models/api/stardust/participation/IParticipationEventResponse"; +import { IParticipationEventStatus } from "../../models/api/stardust/participation/IParticipationEventStatus"; +import { INetwork } from "../../models/db/INetwork"; +import { HexHelper } from "../../utils/hexHelper"; +import { SearchExecutor } from "../../utils/stardust/searchExecutor"; +import { SearchQuery, SearchQueryBuilder } from "../../utils/stardust/searchQueryBuilder"; +import { addressBalance, blockIdFromMilestonePayload } from "../../utils/stardust/utils"; + +/** + * Helper functions for use with tangle. + */ +export class StardustApiService { + /** + * 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 the address details from iotajs. + * @param addressBech32 The address to get the details for in bech32 format. + * @returns The address details. + */ + public async addressDetails(addressBech32: string): Promise { + const { bechHrp } = this.network; + const searchQuery: SearchQuery = new SearchQueryBuilder(addressBech32, bechHrp).build(); + + if (!searchQuery.address) { + return undefined; + } + + try { + // Using ported balance from iota.js until it is added to iota-sdk https://github.com/iotaledger/iota-sdk/issues/604 + const addressBalanceDetails = await addressBalance(this.client, searchQuery.address.bech32); + + if (addressBalanceDetails) { + const addressDetails = { + ...addressBalanceDetails, + hex: searchQuery.address.hex, + bech32: searchQuery.address.bech32, + type: searchQuery.address.type, + }; + + return addressDetails; + } + } catch {} + } + + /** + * 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 transaction included block. + * @param transactionId The transaction id to get the details. + * @returns The item details. + */ + public async transactionIncludedBlock(transactionId: string): Promise { + transactionId = HexHelper.addPrefix(transactionId); + try { + const block = await this.client.getIncludedBlock(transactionId); + + if (!block) { + return { error: `Couldn't find block from transaction id ${transactionId}` }; + } + if (block && Object.keys(block).length > 0) { + return { + block, + }; + } + } catch (e) { + logger.error(`Failed fetching block with transaction id ${transactionId}. Cause: ${e}`); + return { error: "Block 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 outputs details. + * @param outputIds The output ids to get the details. + * @returns The item details. + */ + public async outputsDetails(outputIds: string[]): Promise { + const promises: Promise[] = []; + const outputResponses: OutputResponse[] = []; + + for (const outputId of outputIds) { + const promise = this.outputDetails(outputId); + promises.push(promise); + } + try { + await Promise.all(promises).then((results) => { + for (const outputDetails of results) { + if (outputDetails.output?.output && outputDetails.output?.metadata) { + outputResponses.push(outputDetails.output); + } + } + }); + + return outputResponses; + } catch (e) { + logger.error(`Fetching outputs details failed. Cause: ${e}`); + } + } + + /** + * Get the milestone details by milestone id. + * @param milestoneId The milestone id to get the details. + * @returns The milestone details. + */ + public async milestoneDetailsById(milestoneId: string): Promise { + try { + const milestonePayload = await this.client.getMilestoneById(milestoneId); + + if (milestonePayload) { + const nodeInfoService = ServiceFactory.get(`node-info-${this.network.network}`); + const protocolVersion = nodeInfoService.getNodeInfo().protocolVersion; + const blockId = blockIdFromMilestonePayload(protocolVersion, milestonePayload); + + return { + blockId, + milestoneId, + milestone: milestonePayload, + }; + } + } catch (e) { + logger.error(`Fetching milestone details failed. Cause: ${e}`); + } + } + + /** + * Get the milestone details by index. + * @param milestoneIndex The milestone index to get the details. + * @returns The milestone details. + */ + public async milestoneDetailsByIndex(milestoneIndex: number): Promise { + try { + const milestonePayload = await this.client.getMilestoneByIndex(milestoneIndex); + + if (milestonePayload) { + const nodeInfoService = ServiceFactory.get(`node-info-${this.network.network}`); + const protocolVersion = nodeInfoService.getNodeInfo().protocolVersion; + + const blockId = blockIdFromMilestonePayload(protocolVersion, milestonePayload); + const milestoneId = Utils.milestoneId(milestonePayload); + + return { + blockId, + milestoneId, + milestone: milestonePayload, + }; + } + } catch (e) { + logger.error(`Fetching milestone details failed. Cause: ${e}`); + } + } + + /** + * Get the relevant basic output details for an address. + * @param addressBech32 The address in bech32 format. + * @returns The basic output details. + */ + public async basicOutputDetailsByAddress(addressBech32: string): Promise { + let cursor: string | undefined; + let outputIds: string[] = []; + + do { + try { + const outputIdsResponse = await this.client.basicOutputIds([{ address: addressBech32 }, { cursor: cursor ?? "" }]); + + outputIds = outputIds.concat(outputIdsResponse.items); + cursor = outputIdsResponse.cursor; + } catch (e) { + logger.error(`Fetching basic output ids failed. Cause: ${e}`); + } + } while (cursor); + + const outputResponses = await this.outputsDetails(outputIds); + + return { + outputs: outputResponses, + }; + } + + /** + * Get the relevant alias output details for an address. + * @param addressBech32 The address in bech32 format. + * @returns The alias output details. + */ + public async aliasOutputDetailsByAddress(addressBech32: string): Promise { + let cursor: string | undefined; + let outputIds: string[] = []; + + do { + try { + const outputIdsResponse = await this.client.aliasOutputIds([{ stateController: addressBech32 }, { cursor: cursor ?? "" }]); + + outputIds = outputIds.concat(outputIdsResponse.items); + cursor = outputIdsResponse.cursor; + } catch (e) { + logger.error(`Fetching alias output ids failed. Cause: ${e}`); + } + } while (cursor); + + const outputResponses = await this.outputsDetails(outputIds); + + return { + outputs: outputResponses, + }; + } + + /** + * Get the relevant nft output details for an address. + * @param addressBech32 The address in bech32 format. + * @returns The alias output details. + */ + public async nftOutputDetailsByAddress(addressBech32: string): Promise { + let cursor: string | undefined; + let outputIds: string[] = []; + + do { + try { + const outputIdsResponse = await this.client.nftOutputIds([{ address: addressBech32 }, { cursor: cursor ?? "" }]); + + outputIds = outputIds.concat(outputIdsResponse.items); + cursor = outputIdsResponse.cursor; + } catch (e) { + logger.error(`Fetching nft output ids failed. Cause: ${e}`); + } + } while (cursor); + + const outputResponses = await this.outputsDetails(outputIds); + return { + outputs: outputResponses, + }; + } + + /** + * Get the alias details. + * @param aliasId The aliasId to get the details for. + * @returns The alias details. + */ + public async aliasDetails(aliasId: string): Promise { + try { + const aliasOutputId = await this.client.aliasOutputId(aliasId); + + if (aliasOutputId) { + const outputResponse = await this.outputDetails(aliasOutputId); + + return outputResponse.error ? { error: outputResponse.error } : { aliasDetails: outputResponse.output }; + } + } catch { + return { message: "Alias output not found" }; + } + } + + /** + * Get controlled Foundry output id by controller Alias address + * @param aliasAddress The alias address to get the controlled Foundries for. + * @returns The foundry outputs. + */ + public async aliasFoundries(aliasAddress: string): Promise { + try { + const response = await this.client.foundryOutputIds([{ aliasAddress }]); + + if (response) { + return { + foundryOutputsResponse: response, + }; + } + + return { message: "Foundries output not found" }; + } catch {} + } + + /** + * Get the foundry details. + * @param foundryId The foundryId to get the details for. + * @returns The foundry details. + */ + public async foundryDetails(foundryId: string): Promise { + try { + const foundryOutputId = await this.client.foundryOutputId(foundryId); + + if (foundryOutputId) { + const outputResponse = await this.outputDetails(foundryOutputId); + + return outputResponse.error ? { error: outputResponse.error } : { foundryDetails: outputResponse.output }; + } + return { message: "Foundry output not found" }; + } catch {} + } + + /** + * Get the nft details by nftId. + * @param nftId The nftId to get the details for. + * @returns The nft details. + */ + public async nftDetails(nftId: string): Promise { + try { + const nftOutputId = await this.client.nftOutputId(nftId); + + if (nftOutputId) { + const outputResponse = await this.outputDetails(nftOutputId); + + return outputResponse.error ? { error: outputResponse.error } : { nftDetails: outputResponse.output }; + } + + return { message: "Nft output not found" }; + } catch {} + } + + /** + * Get the basic output Ids with specific tag feature. + * @param encodedTag The tag hex. + * @param pageSize The page size. + * @param cursor The cursor for pagination. + * @returns The basic outputs response. + */ + public async taggedBasicOutputs( + encodedTag: HexEncodedString, + pageSize: number, + cursor?: string, + ): Promise { + try { + const params: NftQueryParameter[] = [{ tag: encodedTag }, { pageSize }, { cursor: cursor ?? "" }]; + const basicOutputIdsResponse: IOutputsResponse = await this.client.basicOutputIds(params); + + if (basicOutputIdsResponse?.items.length > 0) { + return { outputs: basicOutputIdsResponse }; + } + } catch {} + + return { error: `Basic outputs not found with given tag ${encodedTag}` }; + } + + /** + * Get the nft output Ids with specific tag feature. + * @param encodedTag The tag hex. + * @param pageSize The page size. + * @param cursor The cursor for pagination. + * @returns The nft outputs response. + */ + public async taggedNftOutputs( + encodedTag: HexEncodedString, + pageSize: number, + cursor?: string, + ): Promise { + try { + const params: NftQueryParameter[] = [{ tag: encodedTag }, { pageSize }, { cursor: cursor ?? "" }]; + const nftOutputIdsResponse: IOutputsResponse = await this.client.nftOutputIds(params); + + if (nftOutputIdsResponse?.items.length > 0) { + return { outputs: nftOutputIdsResponse }; + } + } catch {} + + return { error: `Nft outputs not found with given tag ${encodedTag}` }; + } + + /** + * Get the output Ids (basic/nft) with specific tag feature. + * @param tag The tag hex. + * @returns . + */ + public async taggedOutputs(tag: HexEncodedString): Promise { + const basicOutputs = await this.taggedBasicOutputs(tag, 10); + const nftOutputs = await this.taggedNftOutputs(tag, 10); + + return { + basicOutputs, + nftOutputs, + }; + } + + /** + * Get the relevant nft output details for an address. + * @param eventId The id of the event. + * @returns The participation event details. + */ + public async participationEventDetails(eventId: string): Promise { + const basePluginPath: string = "api/participation/v1/"; + const method = "GET"; + const methodPath: string = `events/${eventId}`; + const info = await this.nodePluginFetch(basePluginPath, method, methodPath); + const status = await this.nodePluginFetch(basePluginPath, method, `${methodPath}/status`); + + return { + info, + status, + }; + } + + /** + * Find item on the stardust network. + * @param query The query to use for finding items. + * @returns The item found. + */ + public async search(query: string): Promise { + return new SearchExecutor(this.network, new SearchQueryBuilder(query, this.network.bechHrp).build()).run(); + } + + /** + * Extension method which provides request methods for plugins. + * @param basePluginPath The base path for the plugin eg indexer/v1/ . + * @param method The http method. + * @param methodPath The path for the plugin request. + * @param queryParams Additional query params for the request. + * @param request The request object. + * @returns The response object. + */ + private async nodePluginFetch( + basePluginPath: string, + method: "GET" | "POST", + methodPath: string, + queryParams?: string[], + request?: string, + ): Promise | null { + const client = this.client; + + try { + const response: S = (await client.callPluginRoute(basePluginPath, method, methodPath, queryParams, request)) as S; + + return response; + } catch {} + + return null; + } +} diff --git a/api/src/utils/stardust/searchExecutor.ts b/api/src/utils/stardust/searchExecutor.ts index 7fb1c33dc..912676a05 100644 --- a/api/src/utils/stardust/searchExecutor.ts +++ b/api/src/utils/stardust/searchExecutor.ts @@ -1,37 +1,35 @@ -import { OutputResponse } from "@iota/sdk"; import { SearchQuery } from "./searchQueryBuilder"; -import { StardustTangleHelper } from "./stardustTangleHelper"; +import { ServiceFactory } from "../../factories/serviceFactory"; import { ISearchResponse } from "../../models/api/stardust/ISearchResponse"; import { INetwork } from "../../models/db/INetwork"; +import { StardustApiService } from "../../services/stardust/stardustApiService"; /** * Performs the search from a SearchQuery object on a Stardust network. */ export class SearchExecutor { - /** - * The network to search on. - */ - private readonly network: INetwork; - /** * The search query. */ private readonly query: SearchQuery; + private readonly apiService: StardustApiService; + constructor(network: INetwork, query: SearchQuery) { - this.network = network; this.query = query; + this.apiService = ServiceFactory.get(`api-service-${network.network}`); } public async run(): Promise { - const network = this.network; const searchQuery = this.query; const promises: Promise[] = []; let promisesResult: ISearchResponse | null = null; + if (searchQuery.did) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.tryFetchNodeThenPermanode(searchQuery.aliasId, "aliasOutputId", network) + this.apiService + .aliasDetails(searchQuery.aliasId) .then((aliasOutputs) => { if (aliasOutputs) { promisesResult = { @@ -53,7 +51,8 @@ export class SearchExecutor { if (searchQuery.milestoneIndex) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.milestoneDetailsByIndex(network, searchQuery.milestoneIndex) + this.apiService + .milestoneDetailsByIndex(searchQuery.milestoneIndex) .then((milestoneDetails) => { if (milestoneDetails) { promisesResult = { @@ -74,7 +73,8 @@ export class SearchExecutor { if (searchQuery.milestoneId) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.milestoneDetailsById(network, searchQuery.milestoneId) + this.apiService + .milestoneDetailsById(searchQuery.milestoneId) .then((milestoneDetails) => { if (milestoneDetails) { promisesResult = { @@ -95,7 +95,8 @@ export class SearchExecutor { if (searchQuery.blockId) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.block(network, searchQuery.blockId) + this.apiService + .block(searchQuery.blockId) .then((blockResponse) => { if (blockResponse && !blockResponse.error) { promisesResult = { @@ -116,7 +117,8 @@ export class SearchExecutor { if (searchQuery.transactionId) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.transactionIncludedBlock(network, searchQuery.transactionId) + this.apiService + .transactionIncludedBlock(searchQuery.transactionId) .then((txDetailsResponse) => { if (txDetailsResponse.block && Object.keys(txDetailsResponse.block).length > 0) { promisesResult = { @@ -137,10 +139,11 @@ export class SearchExecutor { if (searchQuery.output) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.tryFetchNodeThenPermanode(searchQuery.output, "getOutput", network) + this.apiService + .outputDetails(searchQuery.output) .then((output) => { if (output) { - promisesResult = { output }; + promisesResult = { output: output.output }; resolve(); } else { reject(new Error("Output response not present")); @@ -156,7 +159,8 @@ export class SearchExecutor { if (searchQuery.aliasId) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.tryFetchNodeThenPermanode(searchQuery.aliasId, "aliasOutputId", network) + this.apiService + .aliasDetails(searchQuery.aliasId) .then((aliasOutputs) => { if (aliasOutputs) { promisesResult = { @@ -177,7 +181,8 @@ export class SearchExecutor { if (searchQuery.nftId) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.tryFetchNodeThenPermanode(searchQuery.nftId, "nftOutputId", network) + this.apiService + .nftDetails(searchQuery.nftId) .then((nftOutputs) => { if (nftOutputs) { promisesResult = { @@ -198,7 +203,8 @@ export class SearchExecutor { if (searchQuery.foundryId) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.tryFetchNodeThenPermanode(searchQuery.foundryId, "foundryOutputId", network) + this.apiService + .foundryDetails(searchQuery.foundryId) .then((foundryOutput) => { if (foundryOutput) { promisesResult = { @@ -219,7 +225,8 @@ export class SearchExecutor { if (searchQuery.tag) { promises.push( new Promise((resolve, reject) => { - StardustTangleHelper.taggedOutputs(network, searchQuery.tag) + this.apiService + .taggedOutputs(searchQuery.tag) .then((response) => { if (!response.basicOutputs.error || !response.nftOutputs.error) { promisesResult = { diff --git a/api/src/utils/stardust/stardustTangleHelper.ts b/api/src/utils/stardust/stardustTangleHelper.ts deleted file mode 100644 index 3d0b22dba..000000000 --- a/api/src/utils/stardust/stardustTangleHelper.ts +++ /dev/null @@ -1,569 +0,0 @@ -/* eslint-disable no-warning-comments */ -import { - __ClientMethods__, - OutputResponse, - Client, - IBlockMetadata, - MilestonePayload, - IOutputsResponse, - HexEncodedString, - Block, - Utils, - QueryParameter, - NftQueryParameter, - AliasQueryParameter, - FoundryQueryParameter, -} from "@iota/sdk"; -import { SearchExecutor } from "./searchExecutor"; -import { SearchQueryBuilder, SearchQuery } from "./searchQueryBuilder"; -import { addressBalance, blockIdFromMilestonePayload } from "./utils"; -import { ServiceFactory } from "../../factories/serviceFactory"; -import logger from "../../logger"; -import { IBasicOutputsResponse } from "../../models/api/stardust/basic/IBasicOutputsResponse"; -import { IFoundriesResponse } from "../../models/api/stardust/foundry/IFoundriesResponse"; -import { IFoundryResponse } from "../../models/api/stardust/foundry/IFoundryResponse"; -import { IAddressDetailsResponse } from "../../models/api/stardust/IAddressDetailsResponse"; -import IAddressDetailsWithBalance from "../../models/api/stardust/IAddressDetailsWithBalance"; -import { IAliasResponse } from "../../models/api/stardust/IAliasResponse"; -import { IBlockDetailsResponse } from "../../models/api/stardust/IBlockDetailsResponse"; -import { IBlockResponse } from "../../models/api/stardust/IBlockResponse"; -import { IOutputDetailsResponse } from "../../models/api/stardust/IOutputDetailsResponse"; -import { ISearchResponse } from "../../models/api/stardust/ISearchResponse"; -import { ITaggedOutputsResponse } from "../../models/api/stardust/ITaggedOutputsResponse"; -import { ITransactionDetailsResponse } from "../../models/api/stardust/ITransactionDetailsResponse"; -import { IMilestoneDetailsResponse } from "../../models/api/stardust/milestone/IMilestoneDetailsResponse"; -import { INftDetailsResponse } from "../../models/api/stardust/nft/INftDetailsResponse"; -import { INftOutputsResponse } from "../../models/api/stardust/nft/INftOutputsResponse"; -import { IParticipationEventInfo } from "../../models/api/stardust/participation/IParticipationEventInfo"; -import { IParticipationEventResponse } from "../../models/api/stardust/participation/IParticipationEventResponse"; -import { IParticipationEventStatus } from "../../models/api/stardust/participation/IParticipationEventStatus"; -import { INetwork } from "../../models/db/INetwork"; -import { NodeInfoService } from "../../services/stardust/nodeInfoService"; -import { HexHelper } from "../hexHelper"; - -type NameType = T extends { name: infer U } ? U : never; -type ExtractedMethodNames = NameType<__ClientMethods__>; - -/** - * Helper functions for use with tangle. - */ -export class StardustTangleHelper { - /** - * Get the address details from iotajs. - * @param network The network in context. - * @param addressBech32 The address to get the details for in bech32 format. - * @returns The address details. - */ - public static async addressDetails(network: INetwork, addressBech32: string): Promise { - const { bechHrp } = network; - const client = ServiceFactory.get(`client-${network.network}`); - const searchQuery: SearchQuery = new SearchQueryBuilder(addressBech32, bechHrp).build(); - - if (!searchQuery.address) { - return undefined; - } - - try { - // Using ported balance from iota.js until it is added to iota-sdk https://github.com/iotaledger/iota-sdk/issues/604 - const addressBalanceDetails = await addressBalance(client, searchQuery.address.bech32); - - if (addressBalanceDetails) { - const addressDetails = { - ...addressBalanceDetails, - hex: searchQuery.address.hex, - bech32: searchQuery.address.bech32, - type: searchQuery.address.type, - }; - - return addressDetails; - } - } catch {} - } - - /** - * 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 transaction included block. - * @param network The network to find the items on. - * @param transactionId The transaction id to get the details. - * @returns The item details. - */ - public static async transactionIncludedBlock(network: INetwork, transactionId: string): Promise { - transactionId = HexHelper.addPrefix(transactionId); - const block = await this.tryFetchNodeThenPermanode(transactionId, "getIncludedBlock", network); - - if (!block) { - return { error: `Couldn't find block from transaction id ${transactionId}` }; - } - - try { - if (block && Object.keys(block).length > 0) { - return { - block, - }; - } - } catch (e) { - logger.error(`Failed fetching block with transaction id ${transactionId}. Cause: ${e}`); - } - } - - /** - * 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 outputs details. - * @param network The network to find the items on. - * @param outputIds The output ids to get the details. - * @returns The item details. - */ - public static async outputsDetails(network: INetwork, outputIds: string[]): Promise { - const promises: Promise[] = []; - const outputResponses: OutputResponse[] = []; - - for (const outputId of outputIds) { - const promise = this.outputDetails(network, outputId); - promises.push(promise); - } - try { - await Promise.all(promises).then((results) => { - for (const outputDetails of results) { - if (outputDetails.output?.output && outputDetails.output?.metadata) { - outputResponses.push(outputDetails.output); - } - } - }); - - return outputResponses; - } catch (e) { - logger.error(`Fetching outputs details failed. Cause: ${e}`); - } - } - - /** - * Get the milestone details by milestone id. - * @param network The network to find the items on. - * @param milestoneId The milestone id to get the details. - * @returns The milestone details. - */ - public static async milestoneDetailsById(network: INetwork, milestoneId: string): Promise { - const milestonePayload = await this.tryFetchNodeThenPermanode(milestoneId, "getMilestoneById", network); - - if (milestonePayload) { - const nodeInfoService = ServiceFactory.get(`node-info-${network.network}`); - const protocolVersion = nodeInfoService.getNodeInfo().protocolVersion; - const blockId = blockIdFromMilestonePayload(protocolVersion, milestonePayload); - - return { - blockId, - milestoneId, - milestone: milestonePayload, - }; - } - } - - /** - * Get the milestone details by index. - * @param network The network to find the items on. - * @param milestoneIndex The milestone index to get the details. - * @returns The milestone details. - */ - public static async milestoneDetailsByIndex(network: INetwork, milestoneIndex: number): Promise { - const milestonePayload = await this.tryFetchNodeThenPermanode( - milestoneIndex, - "getMilestoneByIndex", - network, - ); - - if (milestonePayload) { - const nodeInfoService = ServiceFactory.get(`node-info-${network.network}`); - const protocolVersion = nodeInfoService.getNodeInfo().protocolVersion; - - const blockId = blockIdFromMilestonePayload(protocolVersion, milestonePayload); - const milestoneId = Utils.milestoneId(milestonePayload); - - return { - blockId, - milestoneId, - milestone: milestonePayload, - }; - } - } - - /** - * Get the relevant basic output details for an address. - * @param network The network to find the items on. - * @param addressBech32 The address in bech32 format. - * @returns The basic output details. - */ - public static async basicOutputDetailsByAddress(network: INetwork, addressBech32: string): Promise { - let cursor: string | undefined; - let outputIds: string[] = []; - - do { - const outputIdsResponse = await this.tryFetchNodeThenPermanode( - [{ address: addressBech32 }, { cursor: cursor ?? "" }], - "basicOutputIds", - network, - ); - - outputIds = outputIds.concat(outputIdsResponse.items); - cursor = outputIdsResponse.cursor; - } while (cursor); - - const outputResponses = await this.outputsDetails(network, outputIds); - - return { - outputs: outputResponses, - }; - } - - /** - * Get the relevant alias output details for an address. - * @param network The network to find the items on. - * @param addressBech32 The address in bech32 format. - * @returns The alias output details. - */ - public static async aliasOutputDetailsByAddress(network: INetwork, addressBech32: string): Promise { - let cursor: string | undefined; - let outputIds: string[] = []; - - do { - const outputIdsResponse = await this.tryFetchNodeThenPermanode( - [{ stateController: addressBech32 }, { cursor: cursor ?? "" }], - "aliasOutputIds", - network, - ); - - outputIds = outputIds.concat(outputIdsResponse.items); - cursor = outputIdsResponse.cursor; - } while (cursor); - - const outputResponses = await this.outputsDetails(network, outputIds); - - return { - outputs: outputResponses, - }; - } - - /** - * Get the relevant nft output details for an address. - * @param network The network to find the items on. - * @param addressBech32 The address in bech32 format. - * @returns The alias output details. - */ - public static async nftOutputDetailsByAddress(network: INetwork, addressBech32: string): Promise { - let cursor: string | undefined; - let outputIds: string[] = []; - - do { - const outputIdsResponse = await this.tryFetchNodeThenPermanode( - [{ address: addressBech32 }, { cursor: cursor ?? "" }], - "nftOutputIds", - network, - ); - - outputIds = outputIds.concat(outputIdsResponse.items); - cursor = outputIdsResponse.cursor; - } while (cursor); - - const outputResponses = await this.outputsDetails(network, outputIds); - return { - outputs: outputResponses, - }; - } - - /** - * Get the alias details. - * @param network The network to find the items on. - * @param aliasId The aliasId to get the details for. - * @returns The alias details. - */ - public static async aliasDetails(network: INetwork, aliasId: string): Promise { - const aliasOutputId = await this.tryFetchNodeThenPermanode(aliasId, "aliasOutputId", network); - - if (aliasOutputId) { - const outputResponse = await this.outputDetails(network, aliasOutputId); - - return outputResponse.error ? { error: outputResponse.error } : { aliasDetails: outputResponse.output }; - } - - return { message: "Alias output not found" }; - } - - /** - * Get controlled Foundry output id by controller Alias address - * @param network The network to find the items on. - * @param aliasAddress The alias address to get the controlled Foundries for. - * @returns The foundry outputs. - */ - public static async aliasFoundries(network: INetwork, aliasAddress: string): Promise { - try { - const response = await this.tryFetchNodeThenPermanode( - [{ aliasAddress }], - "foundryOutputIds", - network, - ); - - if (response) { - return { - foundryOutputsResponse: response, - }; - } - - return { message: "Foundries output not found" }; - } catch {} - } - - /** - * Get the foundry details. - * @param network The network to find the items on. - * @param foundryId The foundryId to get the details for. - * @returns The foundry details. - */ - public static async foundryDetails(network: INetwork, foundryId: string): Promise { - const foundryOutputId = await this.tryFetchNodeThenPermanode(foundryId, "foundryOutputId", network); - - if (foundryOutputId) { - const outputResponse = await this.outputDetails(network, foundryOutputId); - - return outputResponse.error ? { error: outputResponse.error } : { foundryDetails: outputResponse.output }; - } - - return { message: "Foundry output not found" }; - } - - /** - * Get the nft details by nftId. - * @param network The network to find the items on. - * @param nftId The nftId to get the details for. - * @returns The nft details. - */ - public static async nftDetails(network: INetwork, nftId: string): Promise { - try { - const nftOutputId = await this.tryFetchNodeThenPermanode(nftId, "nftOutputId", network); - - if (nftOutputId) { - const outputResponse = await this.outputDetails(network, nftOutputId); - - return outputResponse.error ? { error: outputResponse.error } : { nftDetails: outputResponse.output }; - } - - return { message: "Nft output not found" }; - } catch {} - } - - /** - * Get the basic output Ids with specific tag feature. - * @param network The network to find the items on. - * @param encodedTag The tag hex. - * @param pageSize The page size. - * @param cursor The cursor for pagination. - * @returns The basic outputs response. - */ - public static async taggedBasicOutputs( - network: INetwork, - encodedTag: HexEncodedString, - pageSize: number, - cursor?: string, - ): Promise { - try { - const params: NftQueryParameter[] = [{ tag: encodedTag }, { pageSize }, { cursor: cursor ?? "" }]; - const basicOutputIdsResponse: IOutputsResponse = await this.tryFetchNodeThenPermanode( - params, - "basicOutputIds", - network, - ); - - if (basicOutputIdsResponse?.items.length > 0) { - return { outputs: basicOutputIdsResponse }; - } - } catch {} - - return { error: `Basic outputs not found with given tag ${encodedTag}` }; - } - - /** - * Get the nft output Ids with specific tag feature. - * @param network The network to find the items on. - * @param encodedTag The tag hex. - * @param pageSize The page size. - * @param cursor The cursor for pagination. - * @returns The nft outputs response. - */ - public static async taggedNftOutputs( - network: INetwork, - encodedTag: HexEncodedString, - pageSize: number, - cursor?: string, - ): Promise { - try { - const params: NftQueryParameter[] = [{ tag: encodedTag }, { pageSize }, { cursor: cursor ?? "" }]; - const nftOutputIdsResponse: IOutputsResponse = await this.tryFetchNodeThenPermanode( - params, - "nftOutputIds", - network, - ); - - if (nftOutputIdsResponse?.items.length > 0) { - return { outputs: nftOutputIdsResponse }; - } - } catch {} - - return { error: `Nft outputs not found with given tag ${encodedTag}` }; - } - - /** - * Get the output Ids (basic/nft) with specific tag feature. - * @param network The network to find the items on. - * @param tag The tag hex. - * @returns . - */ - public static async taggedOutputs(network: INetwork, tag: HexEncodedString): Promise { - const basicOutputs = await this.taggedBasicOutputs(network, tag, 10); - const nftOutputs = await this.taggedNftOutputs(network, tag, 10); - - return { - basicOutputs, - nftOutputs, - }; - } - - /** - * Get the relevant nft output details for an address. - * @param network The network to find the items on. - * @param eventId The id of the event. - * @returns The participation event details. - */ - public static async participationEventDetails(network: INetwork, eventId: string): Promise { - const basePluginPath: string = "api/participation/v1/"; - const method = "GET"; - const methodPath: string = `events/${eventId}`; - const info = await this.nodePluginFetch(network, basePluginPath, method, methodPath); - const status = await this.nodePluginFetch(network, basePluginPath, method, `${methodPath}/status`); - - return { - info, - status, - }; - } - - /** - * Find item on the stardust network. - * @param network The network config. - * @param query The query to use for finding items. - * @returns The item found. - */ - public static async search(network: INetwork, query: string): Promise { - return new SearchExecutor(network, new SearchQueryBuilder(query, network.bechHrp).build()).run(); - } - - /** - * 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; - } - - /** - * Extension method which provides request methods for plugins. - * @param network The network config in context. - * @param basePluginPath The base path for the plugin eg indexer/v1/ . - * @param method The http method. - * @param methodPath The path for the plugin request. - * @param queryParams Additional query params for the request. - * @param request The request object. - * @returns The response object. - */ - private static async nodePluginFetch( - network: INetwork, - basePluginPath: string, - method: "GET" | "POST", - methodPath: string, - queryParams?: string[], - request?: string, - ): Promise | null { - const client = ServiceFactory.get(`client-${network.network}`); - - try { - const response: S = (await client.callPluginRoute(basePluginPath, method, methodPath, queryParams, request)) as S; - - return response; - } catch {} - - return null; - } -}