diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a99abc500..010496dbe 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,7 +2,7 @@ name: Found a bug? description: Fill in this form to report it, and help us improve title: '[Bug]: ' -labels: type:bug report +labels: 'bug report' body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/create-task.yml b/.github/ISSUE_TEMPLATE/create-task.yml index 8df1cdb86..cb8ec44d0 100644 --- a/.github/ISSUE_TEMPLATE/create-task.yml +++ b/.github/ISSUE_TEMPLATE/create-task.yml @@ -19,32 +19,3 @@ body: description: Describe the task that needs to be completed. validations: required: true - - - type: textarea - id: requirements - attributes: - label: Requirements - description: What are the requirements for this task, this could be a checklist of subtasks. - validations: - required: true - - - type: textarea - id: acceptance_criteria - attributes: - label: Acceptance criteria - description: What is the criteria for this task to be marked as done? This will help anyone approving any PRs related to this task. - validations: - required: true - - - type: checkboxes - id: checklist - attributes: - label: Creation checklist - description: 'Before submitting this task please ensure you have done the following if necessary:' - options: - - label: I have assigned this task to the correct people - required: false - - label: I have added the most appropriate labels - required: false - - label: I have linked the correct milestone and/or project - required: false diff --git a/.github/workflows/add-bugs-to-project.yaml b/.github/workflows/add-bugs-to-project.yaml new file mode 100644 index 000000000..bc992d607 --- /dev/null +++ b/.github/workflows/add-bugs-to-project.yaml @@ -0,0 +1,19 @@ +name: Assign Bugs to Project + +on: + issues: + types: [labeled] +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + assign_to_project: + runs-on: ubuntu-latest + name: Assign bug issue to Bug Management project + steps: + - name: Assign issues with bug report label to 'Tooling - Bug Management' project + uses: srggrs/assign-one-project-github-action@4fc2b23bdaaac08e64dcc590a6e138d2f9b8c86e + if: contains(github.event.issue.labels.*.name, 'bug report') + with: + project: 'https://github.com/orgs/iotaledger/projects/78' + column_name: 'Triage' diff --git a/.github/workflows/pr-build-check.yaml b/.github/workflows/pr-build-check.yaml new file mode 100644 index 000000000..2236d061a --- /dev/null +++ b/.github/workflows/pr-build-check.yaml @@ -0,0 +1,29 @@ +name: PR build check + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + build-check: + strategy: + fail-fast: false + matrix: + project: [api, client] + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ matrix.project }} + steps: + - uses: actions/checkout@v2 + + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: "16" + + - name: Install Dependencies + run: npm install + + - name: Build Check + run: npm run build diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index b4c0b12de..000000000 --- a/.husky/pre-push +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npm run pre-push 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; - } -} diff --git a/client/src/app/components/stardust/address/section/native-tokens/AssetsTable.tsx b/client/src/app/components/stardust/address/section/native-tokens/AssetsTable.tsx index ad6f4b299..ebfe3abbc 100644 --- a/client/src/app/components/stardust/address/section/native-tokens/AssetsTable.tsx +++ b/client/src/app/components/stardust/address/section/native-tokens/AssetsTable.tsx @@ -35,10 +35,12 @@ const AssetsTable: React.FC = ({ networkId, outputs, setTokenC ) { for (const token of (output as CommonOutput).nativeTokens ?? []) { const existingToken = theTokens.find((t) => t.id === token.id); + // Convert to BigInt again in case the amount is hex + const amount = BigInt(token.amount); if (existingToken) { - existingToken.amount += token.amount; + existingToken.amount += amount; } else { - theTokens.push({ id: token.id, amount: token.amount }); + theTokens.push({ id: token.id, amount }); } } } diff --git a/client/src/app/components/stardust/foundry/TokenInfoSection.tsx b/client/src/app/components/stardust/foundry/TokenInfoSection.tsx index e0d189268..629f0974b 100644 --- a/client/src/app/components/stardust/foundry/TokenInfoSection.tsx +++ b/client/src/app/components/stardust/foundry/TokenInfoSection.tsx @@ -29,9 +29,9 @@ const TokenInfoSection: React.FC = ({ tokenId, tokenSchem const simpleTokenScheme = tokenScheme as SimpleTokenScheme; - const maximumSupply = formatNumberWithCommas(simpleTokenScheme.maximumSupply); - const mintedTokens = formatNumberWithCommas(simpleTokenScheme.mintedTokens); - const meltedTokens = formatNumberWithCommas(simpleTokenScheme.meltedTokens); + const maximumSupply = formatNumberWithCommas(BigInt(simpleTokenScheme.maximumSupply)); + const mintedTokens = formatNumberWithCommas(BigInt(simpleTokenScheme.mintedTokens)); + const meltedTokens = formatNumberWithCommas(BigInt(simpleTokenScheme.meltedTokens)); return (
diff --git a/client/src/app/components/stardust/history/TransactionCard.tsx b/client/src/app/components/stardust/history/TransactionCard.tsx index e48e9342a..efac2b37a 100644 --- a/client/src/app/components/stardust/history/TransactionCard.tsx +++ b/client/src/app/components/stardust/history/TransactionCard.tsx @@ -1,57 +1,45 @@ import classNames from "classnames"; -import moment from "moment"; -import React, { useContext } from "react"; -import { ITransactionEntryProps } from "./TransactionEntryProps"; -import { DateHelper } from "~helpers/dateHelper"; -import { TransactionsHelper } from "~helpers/stardust/transactionsHelper"; -import { formatAmount } from "~helpers/stardust/valueFormatHelper"; -import { CHRYSALIS_MAINNET } from "~models/config/networkType"; -import NetworkContext from "../../../context/NetworkContext"; +import React from "react"; import TruncatedId from "../TruncatedId"; +import Tooltip from "~app/components/Tooltip"; +import { ITransactionEntryProps } from "./TransactionEntryProps"; const TransactionCard: React.FC = ({ - outputId, + isGenesisByDate, + isTransactionFromStardustGenesis, + transactionLink, + dateFormatted, + balanceChangeFormatted, transactionId, - date, - milestoneIndex, - value, isSpent, isFormattedAmounts, setIsFormattedAmounts, }) => { - const { name: network, tokenInfo } = useContext(NetworkContext); - const ago = moment(date * 1000).fromNow(); - const valueView = ( setIsFormattedAmounts(!isFormattedAmounts)}> - {`${isSpent ? "-" : "+"} ${formatAmount(value, tokenInfo, !isFormattedAmounts)}`} + {balanceChangeFormatted} ); - const isTransactionFromStardustGenesis = - milestoneIndex && TransactionsHelper.isTransactionFromIotaStardustGenesis(network, milestoneIndex); - const transactionLink = isTransactionFromStardustGenesis - ? `/${CHRYSALIS_MAINNET}/search/${transactionId}` - : `/${network}/transaction/${transactionId}`; - return (
-
Transaction Id
-
- -
+
Date
+ {isGenesisByDate ?
Genesis
:
{dateFormatted}
}
-
Output Id
+
Transaction Id
- + + {isTransactionFromStardustGenesis && ( + + + warning + + + )}
-
-
Date
-
{`${DateHelper.formatShort(date * 1000)} (${ago})`}
-
Value
{valueView}
diff --git a/client/src/app/components/stardust/history/TransactionEntryProps.ts b/client/src/app/components/stardust/history/TransactionEntryProps.ts index 981519548..08c19a4ca 100644 --- a/client/src/app/components/stardust/history/TransactionEntryProps.ts +++ b/client/src/app/components/stardust/history/TransactionEntryProps.ts @@ -1,47 +1,46 @@ export interface ITransactionEntryProps { /** - * The output id. + * The transaction id. */ - outputId: string; + transactionId: string; /** - * The transaction id. + * The formatted date of the transaction. */ - transactionId: string; + dateFormatted: string; /** - * The date of the transaction. + * Is the transaction spent. */ - date: number; + isSpent: boolean; /** - * The milestone index of the transaction. + * Are the amounts formatted. */ - milestoneIndex: number; + isFormattedAmounts: boolean; /** - * The transaction amount. + * The setter for formatted amounts toggle. */ - value: number; + setIsFormattedAmounts: React.Dispatch>; /** - * Is the transaction spent. + * The formatted transaction amount. */ - isSpent: boolean; + balanceChangeFormatted: string; /** - * Are the amounts formatted. + * Check if transaction from stardust by TransactionHelper. */ - isFormattedAmounts: boolean; + isTransactionFromStardustGenesis: boolean; /** - * The setter for formatted amounts toggle. + * check some of outputs timestamps zero */ - setIsFormattedAmounts: React.Dispatch>; + isGenesisByDate: boolean; /** - * To colour the transaction row ligter/darker, alternating on - * unrelated transactions. + * The transaction link. */ - darkBackgroundRow?: boolean; + transactionLink: string; } diff --git a/client/src/app/components/stardust/history/TransactionHistory.scss b/client/src/app/components/stardust/history/TransactionHistory.scss index 4081bd470..60d53e5ee 100644 --- a/client/src/app/components/stardust/history/TransactionHistory.scss +++ b/client/src/app/components/stardust/history/TransactionHistory.scss @@ -44,8 +44,10 @@ } td { - padding: 16px 0; + padding: 16px; text-align: center; + width: 33%; + text-wrap: nowrap; &.transaction-id, &.output-id { diff --git a/client/src/app/components/stardust/history/TransactionHistory.tsx b/client/src/app/components/stardust/history/TransactionHistory.tsx index 6a1e47611..564c2a736 100644 --- a/client/src/app/components/stardust/history/TransactionHistory.tsx +++ b/client/src/app/components/stardust/history/TransactionHistory.tsx @@ -1,12 +1,14 @@ /* eslint-disable no-void */ -import React, { useEffect, useState } from "react"; -import TransactionCard from "./TransactionCard"; -import TransactionRow from "./TransactionRow"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import { useAddressHistory } from "~helpers/hooks/useAddressHistory"; +import NetworkContext from "~app/context/NetworkContext"; import DownloadModal from "../DownloadModal"; +import { getTransactionHistoryRecords } from "./transactionHistoryUtils"; +import TransactionRow from "./TransactionRow"; +import TransactionCard from "./TransactionCard"; import "./TransactionHistory.scss"; -interface TransactionHistoryProps { +export interface TransactionHistoryProps { readonly network: string; readonly address?: string; readonly setLoading: (isLoadin: boolean) => void; @@ -14,16 +16,25 @@ interface TransactionHistoryProps { } const TransactionHistory: React.FC = ({ network, address, setLoading, setDisabled }) => { - const [historyView, outputDetailsMap, loadMore, isLoading, hasMore] = useAddressHistory(network, address, setDisabled); + const [transactionIdToOutputs, loadMore, isLoading, hasMore] = useAddressHistory(network, address, setDisabled); + const [isFormattedAmounts, setIsFormattedAmounts] = useState(true); + const { tokenInfo } = useContext(NetworkContext); useEffect(() => { setLoading(isLoading); }, [isLoading]); - let isDarkBackgroundRow = false; + const transactions = useMemo(() => { + const transactionsLocal = getTransactionHistoryRecords(transactionIdToOutputs, network, tokenInfo, isFormattedAmounts); + if (hasMore) { + // remove last transaction, as it's potentially doesn't have all outputs + transactionsLocal.pop(); + } + return transactionsLocal; + }, [transactionIdToOutputs, tokenInfo, isFormattedAmounts, hasMore]); - return historyView.length > 0 && address ? ( + return transactions.length > 0 && address ? (
@@ -31,94 +42,51 @@ const TransactionHistory: React.FC = ({ network, addres - - + - {historyView.length > 0 && - historyView.map((historyItem, idx) => { - const outputDetails = outputDetailsMap[historyItem.outputId]; - if (!outputDetails) { - return null; - } - const transactionId = historyItem.isSpent - ? outputDetails.metadata.transactionIdSpent - : outputDetails.metadata.transactionId; - - if (!transactionId) { - return null; - } - - // rotate row background colour for different transaction ids - if (idx > 0) { - const previousItemDetails = outputDetailsMap[historyView[idx - 1].outputId]; - const previousSpent = historyView[idx - 1].isSpent; - if (previousItemDetails) { - const previousTransactionId = previousSpent - ? previousItemDetails.metadata.transactionIdSpent - : previousItemDetails.metadata.transactionId; - - if (transactionId !== previousTransactionId) { - isDarkBackgroundRow = !isDarkBackgroundRow; - } - } - } - - return ( - - - - ); - })} + {transactions?.map((c, idx) => ( + + + + ))}
Transaction IdOutput Id DateTransaction Id Value
- {/* Only visible in mobile -- Card transactions*/} + {/** Only visible in mobile -- Card transactions */}
- {historyView.length > 0 && - historyView.map((historyItem, idx) => { - const outputDetails = outputDetailsMap[historyItem.outputId]; - if (!outputDetails) { - return null; - } - const transactionId = historyItem.isSpent - ? outputDetails.metadata.transactionIdSpent - : outputDetails.metadata.transactionId; - - if (!transactionId) { - return null; - } - - return ( - - - - ); - })} + {transactions.map((c, idx) => { + return ( + + + + ); + })}
- {hasMore && historyView.length > 0 && ( + {hasMore && transactions.length > 0 && (
diff --git a/client/src/app/components/stardust/history/TransactionRow.tsx b/client/src/app/components/stardust/history/TransactionRow.tsx index 1bc2b6dd5..de540d02b 100644 --- a/client/src/app/components/stardust/history/TransactionRow.tsx +++ b/client/src/app/components/stardust/history/TransactionRow.tsx @@ -1,46 +1,30 @@ import classNames from "classnames"; -import moment from "moment"; -import React, { useContext } from "react"; +import React from "react"; import { Link } from "react-router-dom"; -import { ITransactionEntryProps } from "./TransactionEntryProps"; -import { DateHelper } from "~helpers/dateHelper"; -import { TransactionsHelper } from "~helpers/stardust/transactionsHelper"; -import { formatAmount } from "~helpers/stardust/valueFormatHelper"; -import { CHRYSALIS_MAINNET } from "~models/config/networkType"; -import NetworkContext from "../../../context/NetworkContext"; import Tooltip from "../../Tooltip"; import TruncatedId from "../TruncatedId"; +import { ITransactionEntryProps } from "./TransactionEntryProps"; const TransactionRow: React.FC = ({ - outputId, + isGenesisByDate, + isTransactionFromStardustGenesis, + transactionLink, + dateFormatted, + balanceChangeFormatted, transactionId, - date, - milestoneIndex, - value, isSpent, isFormattedAmounts, setIsFormattedAmounts, - darkBackgroundRow, }) => { - const { name: network, tokenInfo } = useContext(NetworkContext); - const outputIdTransaction = outputId.slice(0, -4); - const outputIdIndex = outputId.slice(-4); - const ago = moment(date * 1000).fromNow(); - const valueView = ( setIsFormattedAmounts(!isFormattedAmounts)}> - {`${isSpent ? "-" : "+"} ${formatAmount(value, tokenInfo, !isFormattedAmounts)}`} + {balanceChangeFormatted} ); - const isTransactionFromStardustGenesis = - milestoneIndex && TransactionsHelper.isTransactionFromIotaStardustGenesis(network, milestoneIndex); - const transactionLink = isTransactionFromStardustGenesis - ? `/${CHRYSALIS_MAINNET}/search/${transactionId}` - : `/${network}/transaction/${transactionId}`; - return ( - + + {isGenesisByDate ? Genesis : {dateFormatted}} @@ -53,13 +37,6 @@ const TransactionRow: React.FC = ({ )} - - - - - {outputIdIndex} - - {date === 0 ? Genesis : {`${DateHelper.formatShort(date * 1000)} (${ago})`}} {valueView} ); diff --git a/client/src/app/components/stardust/history/transactionHistoryUtils.ts b/client/src/app/components/stardust/history/transactionHistoryUtils.ts new file mode 100644 index 000000000..d5e23a4a3 --- /dev/null +++ b/client/src/app/components/stardust/history/transactionHistoryUtils.ts @@ -0,0 +1,113 @@ +import { CommonOutput, INodeInfoBaseToken } from "@iota/sdk-wasm/web"; +import moment from "moment/moment"; + +import { DateHelper } from "~helpers/dateHelper"; +import { OutputWithDetails } from "~helpers/hooks/useAddressHistory"; +import { TransactionsHelper } from "~helpers/stardust/transactionsHelper"; +import { formatAmount } from "~helpers/stardust/valueFormatHelper"; +import { CHRYSALIS_MAINNET } from "~models/config/networkType"; + +export interface ITransactionHistoryRecord { + isGenesisByDate: boolean; + isTransactionFromStardustGenesis: boolean; + isSpent: boolean; + transactionLink: string; + transactionId: string; + timestamp: number; + dateFormatted: string; + balanceChange: number; + balanceChangeFormatted: string; + outputs: OutputWithDetails[]; +} + +export const groupOutputsByTransactionId = (outputsWithDetails: OutputWithDetails[]) => { + const transactionIdToOutputs = new Map(); + outputsWithDetails.forEach((output) => { + const detailsMetadata = output?.details?.metadata; + if (!detailsMetadata) { + return; + } + + const transactionId = output.isSpent ? detailsMetadata.transactionIdSpent : detailsMetadata.transactionId; + + if (!transactionId) { + return; + } + + const addOutputToTransactionId = (transactionId: string, output: OutputWithDetails) => { + // if we don't have the transaction + const previousOutputs = transactionIdToOutputs.get(transactionId); + if (previousOutputs) { + transactionIdToOutputs.set(transactionId, [...previousOutputs, output]); + } else { + transactionIdToOutputs.set(transactionId, [output]); + } + }; + addOutputToTransactionId(transactionId, output); + }); + + return transactionIdToOutputs; +}; + +export const getTransactionHistoryRecords = ( + transactionIdToOutputs: Map, + network: string, + tokenInfo: INodeInfoBaseToken, + isFormattedAmounts: boolean, +): ITransactionHistoryRecord[] => { + const calculatedTransactions: ITransactionHistoryRecord[] = []; + + transactionIdToOutputs.forEach((outputs, transactionId) => { + const lastOutputTime = Math.max(...outputs.map((t) => t.milestoneTimestamp)); + const balanceChange = calculateBalanceChange(outputs); + const ago = moment(lastOutputTime * 1000).fromNow(); + + const isGenesisByDate = outputs.map((t) => t.milestoneTimestamp).some((milestoneTimestamp) => milestoneTimestamp === 0); + + const milestoneIndexes = outputs.map((t) => t.milestoneIndex); + const isTransactionFromStardustGenesis = milestoneIndexes.some((milestoneIndex) => + TransactionsHelper.isTransactionFromIotaStardustGenesis(network, milestoneIndex), + ); + + const transactionLink = isTransactionFromStardustGenesis + ? `/${CHRYSALIS_MAINNET}/search/${transactionId}` + : `/${network}/transaction/${transactionId}`; + + const isSpent = balanceChange < 0; + + if (balanceChange === 0) { + return; + } + + calculatedTransactions.push({ + isGenesisByDate: isGenesisByDate, + isTransactionFromStardustGenesis: isTransactionFromStardustGenesis, + isSpent: isSpent, + transactionLink: transactionLink, + transactionId: transactionId, + timestamp: lastOutputTime, + dateFormatted: `${DateHelper.formatShort(lastOutputTime * 1000)} (${ago})`, + balanceChange: balanceChange, + balanceChangeFormatted: (isSpent ? `-` : `+`) + formatAmount(Math.abs(balanceChange), tokenInfo, !isFormattedAmounts), + outputs: outputs, + }); + }); + return calculatedTransactions; +}; + +export const calculateBalanceChange = (outputs: OutputWithDetails[]) => { + return outputs.reduce((acc, output) => { + const outputFromDetails = output?.details?.output as CommonOutput; + + if (!outputFromDetails?.amount) { + console.warn("Output details not found for: ", output); + return acc; + } + + let amount = Number(outputFromDetails.amount); + if (output.isSpent) { + amount = -1 * amount; + } + return acc + amount; + }, 0); +}; diff --git a/client/src/helpers/hooks/useAddressHistory.ts b/client/src/helpers/hooks/useAddressHistory.ts index 6ac48f787..ebc0075a5 100644 --- a/client/src/helpers/hooks/useAddressHistory.ts +++ b/client/src/helpers/hooks/useAddressHistory.ts @@ -6,10 +6,9 @@ import { ITransactionHistoryRequest } from "~models/api/stardust/ITransactionHis import { ITransactionHistoryItem, ITransactionHistoryResponse } from "~models/api/stardust/ITransactionHistoryResponse"; import { STARDUST } from "~models/config/protocolVersion"; import { StardustApiClient } from "~services/stardust/stardustApiClient"; +import { groupOutputsByTransactionId } from "~app/components/stardust/history/transactionHistoryUtils"; -interface IOutputDetailsMap { - [outputId: string]: OutputResponse; -} +export type OutputWithDetails = ITransactionHistoryItem & { details: OutputResponse | null; amount?: string }; /** * Fetch Address history @@ -22,99 +21,104 @@ export function useAddressHistory( network: string, address?: string, setDisabled?: (isDisabled: boolean) => void, -): [ITransactionHistoryItem[], IOutputDetailsMap, () => void, boolean, boolean] { +): [Map, () => void, boolean, boolean] { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${STARDUST}`)); - const [history, setHistory] = useState([]); - const [outputDetailsMap, setOutputDetailsMap] = useState({}); - const [historyView, setHistoryView] = useState([]); + const [outputsWithDetails, setOutputsWithDetails] = useState([]); + const [transactionIdToOutputs, setTransactionIdToOutputs] = useState>(new Map()); const [isAddressHistoryLoading, setIsAddressHistoryLoading] = useState(true); const [cursor, setCursor] = useState(); const PAGE_SIZE: number = 10; const SORT: string = "newest"; useEffect(() => { - loadHistory(); - }, [address]); + if (!address || !isMounted) return; + (async () => { + await loadHistory(); + })(); + }, [address, isMounted]); - const loadHistory = () => { - if (address) { - setIsAddressHistoryLoading(true); - const request: ITransactionHistoryRequest = { - network, - address, - pageSize: PAGE_SIZE, - sort: SORT, - cursor, - }; - - apiClient - .transactionHistory(request) - .then((response: ITransactionHistoryResponse | undefined) => { - const items = response?.items ?? []; - if (items.length > 0 && isMounted) { - setHistory([...history, ...items]); - setCursor(response?.cursor); - } else if (setDisabled && isMounted) { - setDisabled(true); - } - }) - .finally(() => { - setIsAddressHistoryLoading(false); - }); + const requestOutputsList = async () => { + if (!address) return; + + const request: ITransactionHistoryRequest = { + network, + address, + pageSize: PAGE_SIZE, + sort: SORT, + cursor, + }; + + const response = (await apiClient.transactionHistory(request)) as ITransactionHistoryResponse | undefined; + const items = response?.items ?? []; + return { + outputs: items, + cursor: response?.cursor, + }; + }; + + const requestOutputDetails = async (outputId: string) => { + if (!outputId) return null; + + try { + const response = await apiClient.outputDetails({ network, outputId }); + const details = response.output; + + if (!response.error && details?.output && details?.metadata) { + return details; + } + return null; + } catch { + console.log("Failed loading transaction history details!"); + return null; } }; - useEffect(() => { - if (history.length > 0) { + const loadHistory = async () => { + if (address && isMounted) { setIsAddressHistoryLoading(true); - const promises: Promise[] = []; - const detailsPage: IOutputDetailsMap = {}; - - for (const item of history) { - const promise = apiClient - .outputDetails({ network, outputId: item.outputId }) - .then((response) => { - const details = response.output; - if (!response.error && details?.output && details?.metadata) { - const outputDetails = { - output: details.output, - metadata: details.metadata, - }; - - detailsPage[item.outputId] = outputDetails; - } - }) - .catch((e) => console.log(e)); - - promises.push(promise); - } - Promise.allSettled(promises) - .then((_) => { - if (isMounted) { - setOutputDetailsMap(detailsPage); - setIsAddressHistoryLoading(false); - const updatedHistoryView = [...history].sort((a, b) => { - // Ensure that entries with equal timestamp, but different isSpent, - // have the spending before the depositing - if (a.milestoneTimestamp === b.milestoneTimestamp && a.isSpent !== b.isSpent) { - return a.isSpent ? 1 : -1; - } - return 1; - }); - - setHistoryView(updatedHistoryView); + try { + const outputList = await requestOutputsList(); + + if (!outputList) { + return; + } + + const { outputs, cursor } = outputList; + + if (!cursor) { + setDisabled?.(true); + } + + const fulfilledOutputs: OutputWithDetails[] = []; + + for (const output of outputs) { + const outputDetails = await requestOutputDetails(output.outputId); + fulfilledOutputs.push({ ...output, details: outputDetails, amount: outputDetails?.output?.amount }); + } + + const updatedOutputsWithDetails = [...outputsWithDetails, ...fulfilledOutputs]; + + updatedOutputsWithDetails.sort((a, b) => { + // Ensure that entries with equal timestamp, but different isSpent, + // have the spending before the depositing + if (a.milestoneTimestamp === b.milestoneTimestamp && a.isSpent !== b.isSpent) { + return a.isSpent ? 1 : -1; } - }) - .catch((_) => { - console.log("Failed loading transaction history details!"); - }) - .finally(() => { - setIsAddressHistoryLoading(false); + return 1; }); + + const groupedOutputsByTransactionId = groupOutputsByTransactionId(updatedOutputsWithDetails); + + setTransactionIdToOutputs(groupedOutputsByTransactionId); + setOutputsWithDetails([...outputsWithDetails, ...fulfilledOutputs]); + setCursor(cursor); + } finally { + setIsAddressHistoryLoading(false); + } } - }, [history]); + }; - return [historyView, outputDetailsMap, loadHistory, isAddressHistoryLoading, Boolean(cursor)]; + return [transactionIdToOutputs, loadHistory, isAddressHistoryLoading, Boolean(cursor)]; } diff --git a/client/src/services/identityService.ts b/client/src/services/identityService.ts index 9bdcc6626..4b53e1c32 100644 --- a/client/src/services/identityService.ts +++ b/client/src/services/identityService.ts @@ -9,6 +9,8 @@ import { CHRYSALIS, STARDUST } from "~models/config/protocolVersion"; import * as identity from "@iota/identity-wasm/web"; export class IdentityService { + private initLibraryPromise: Promise | undefined; + /** * Resolves DID into it's DID document (Chrysalis). * @param {string} did DID to be resolved @@ -66,6 +68,10 @@ export class IdentityService { } public async initLibrary(path = "/wasm/identity_wasm_bg.wasm") { - return await identity.init(path); + if (!this.initLibraryPromise) { + this.initLibraryPromise = identity.init(path); + } + + return this.initLibraryPromise; } } diff --git a/package.json b/package.json index ce1d1e301..2dfbff78c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "setup:dev": "npm run clear && npm run prepare && npm run setup:client && npm run setup:api", "clear": "rimraf api/node_modules api/dist client/node_modules client/build", "dev": "concurrently 'cd api && npm run start-dev' 'cd client && npm run start'", - "pre-push": "concurrently 'cd api && npm run build-compile && npm run build-lint && npm run build-config' 'cd client && npm run build'", "prepare": "husky install", "format": "concurrently 'cd api && npm run format' 'cd client && npm run format'" },