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/models/api/stardust/chronicle/IAddressBalanceResponse.ts b/api/src/models/api/stardust/chronicle/IAddressBalanceResponse.ts index 92fa362e1..4e65c9c7b 100644 --- a/api/src/models/api/stardust/chronicle/IAddressBalanceResponse.ts +++ b/api/src/models/api/stardust/chronicle/IAddressBalanceResponse.ts @@ -9,7 +9,7 @@ export interface IAddressBalanceResponse extends IResponse { /** * The balance of trivialy unlockable outputs with address unlock condition. */ - sigLockedBalance?: number; + availableBalance?: number; /** * The ledger index at which this balance data was valid. 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/App.tsx b/client/src/app/App.tsx index fbfc0f3a7..ab94b1ee2 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -45,7 +45,8 @@ const App: React.FC> = ({ }, [networksLoaded]); const networkConfig = networks.find((n) => n.network === network); - const identityResolverEnabled = networkConfig?.identityResolverEnabled ?? true; + const protocolVersion = networkConfig?.protocolVersion ?? STARDUST; + const identityResolverEnabled = protocolVersion !== STARDUST && (networkConfig?.identityResolverEnabled ?? true); const currentNetworkName = networkConfig?.network; const isShimmer = isShimmerUiTheme(networkConfig?.uiTheme); const nodeService = ServiceFactory.get("node-info"); diff --git a/client/src/app/AppUtils.tsx b/client/src/app/AppUtils.tsx index 7f41f2817..b53930ab1 100644 --- a/client/src/app/AppUtils.tsx +++ b/client/src/app/AppUtils.tsx @@ -28,9 +28,7 @@ export const getPages = (currentNetwork: INetwork | undefined, networks: INetwor const pages = []; if (networks.length > 0 && currentNetwork !== undefined) { pages.push({ label: "Explorer", url: `/${currentNetwork.network}/` }); - if (currentNetwork.network !== CHRYSALIS_MAINNET) { - pages.push({ label: "Visualizer", url: `/${currentNetwork.network}/visualizer/` }); - } + pages.push({ label: "Visualizer", url: `/${currentNetwork.network}/visualizer/` }); if (currentNetwork.hasStatisticsSupport) { pages.push({ label: "Statistics", url: `/${currentNetwork.network}/statistics/` }); @@ -40,6 +38,18 @@ export const getPages = (currentNetwork: INetwork | undefined, networks: INetwor return pages; }; +export const buildUtilities = (currentNetwork: string, networks: INetwork[], identityResolverEnabled: boolean) => { + const utilities = []; + if (networks.length > 0) { + utilities.push({ label: "Streams v0", url: `/${currentNetwork}/streams/0/` }); + if (identityResolverEnabled) { + utilities.push({ label: "Decentralized Identifier", url: `/${currentNetwork}/identity-resolver/` }); + } + } + + return utilities; +}; + /** * Creates footer items. Excludes the Identity Resolver if the network is not supported. * @param currentNetwork The currently selected network. @@ -51,13 +61,11 @@ export const getFooterItems = (currentNetwork: string, networks: INetwork[], ide if (networks.length > 0) { let footerArray = networks.filter((network) => network.isEnabled).map((n) => ({ label: n.label, url: n.network.toString() })); - if (currentNetwork !== CHRYSALIS_MAINNET) { - footerArray = footerArray - .concat({ label: "Streams v0", url: `${currentNetwork}/streams/0/` }) - .concat({ label: "Visualizer", url: `${currentNetwork}/visualizer/` }); - } + footerArray = footerArray + .concat({ label: "Streams v0", url: `${currentNetwork}/streams/0/` }) + .concat({ label: "Visualizer", url: `${currentNetwork}/visualizer/` }); - if (identityResolverEnabled && currentNetwork !== CHRYSALIS_MAINNET) { + if (identityResolverEnabled) { footerArray.push({ label: "Identity Resolver", url: `${currentNetwork}/identity-resolver/` }); } diff --git a/client/src/app/components/JsonViewer.scss b/client/src/app/components/JsonViewer.scss index 95c2549ce..66b5ef475 100644 --- a/client/src/app/components/JsonViewer.scss +++ b/client/src/app/components/JsonViewer.scss @@ -6,6 +6,13 @@ font-family: "Courier New", Courier, monospace; white-space: pre-wrap; + @include font-size(12px, 20px); + + color: var(--did-color); + font-family: $ibm-plex-mono; + letter-spacing: 0.02em; + white-space: pre-wrap; + .string { color: green; } diff --git a/client/src/app/components/identity/IdentityStardustResolver.scss b/client/src/app/components/identity/DIDResolver.scss similarity index 100% rename from client/src/app/components/identity/IdentityStardustResolver.scss rename to client/src/app/components/identity/DIDResolver.scss diff --git a/client/src/app/components/identity/IdentityStardustResolver.tsx b/client/src/app/components/identity/DIDResolver.tsx similarity index 93% rename from client/src/app/components/identity/IdentityStardustResolver.tsx rename to client/src/app/components/identity/DIDResolver.tsx index 3c2dceb43..382fbe090 100644 --- a/client/src/app/components/identity/IdentityStardustResolver.tsx +++ b/client/src/app/components/identity/DIDResolver.tsx @@ -7,17 +7,17 @@ import { LinkedDomainService, } from "@iota/identity-wasm/web"; import React, { Fragment, useEffect, useState } from "react"; -import IdentityDomainResolver from "./domains/IdentityDomainResolver"; -import { IdentityStardustResolverProps } from "./IdentityStardustResolverProps"; +import DIDDomainResolver from "./domains/DIDDomainResolver"; +import { DIDResolverProps } from "./DIDResolverProps"; import { ServiceFactory } from "~factories/serviceFactory"; import { IdentityService } from "~services/identityService"; -import "./IdentityStardustResolver.scss"; +import "./DIDResolver.scss"; import CopyButton from "../CopyButton"; import JsonViewer from "../JsonViewer"; import Spinner from "../Spinner"; -import { IIdentityStardustResolveResponse } from "~/models/api/IIdentityStardustResolveResponse"; +import { IDIDResolverResponse } from "~/models/api/IDIDResolverResponse"; -const IdentityStardustResolver: React.FC = ({ resolvedDID, network }) => { +const IdentityStardustResolver: React.FC = ({ resolvedDID, network }) => { const [DID, setDID] = useState(""); const [governorAddress, setGovernorAddress] = useState(""); const [stateControllerAddress, setStateControllerAddress] = useState(""); @@ -86,7 +86,7 @@ const IdentityStardustResolver: React.FC = ({ res
Linked Domains
- +
)} @@ -146,7 +146,7 @@ const IdentityStardustResolver: React.FC = ({ res // } export default IdentityStardustResolver; -async function constructVerifiedDomains(resolvedDID: IIdentityStardustResolveResponse): Promise>> { +async function constructVerifiedDomains(resolvedDID: IDIDResolverResponse): Promise>> { const newVerifiedDomains = new Map>(); await ServiceFactory.get("identity").initLibrary(); diff --git a/client/src/app/components/identity/DIDResolverProps.ts b/client/src/app/components/identity/DIDResolverProps.ts new file mode 100644 index 000000000..134928fc7 --- /dev/null +++ b/client/src/app/components/identity/DIDResolverProps.ts @@ -0,0 +1,7 @@ +import { IDIDResolverResponse } from "~/models/api/IDIDResolverResponse"; + +export interface DIDResolverProps { + resolvedDID: IDIDResolverResponse | null; + + network: string; +} diff --git a/client/src/app/components/identity/IdentitySearchInput.tsx b/client/src/app/components/identity/IdentitySearchInput.tsx index a5f8522c7..9d2413fd9 100644 --- a/client/src/app/components/identity/IdentitySearchInput.tsx +++ b/client/src/app/components/identity/IdentitySearchInput.tsx @@ -79,21 +79,6 @@ class SearchInput extends AsyncComponent { - constructor(props: IdentityDomainResolverProps) { +class DIDDomainResolver extends AsyncComponent { + constructor(props: DIDDomainResolverProps) { super(props); this.state = { verifiedDomainsPresentation: new Map(), @@ -20,7 +20,7 @@ class IdentityDomainResolver extends AsyncComponent>; } diff --git a/client/src/app/components/identity/domains/IdentityDomainResolverState.ts b/client/src/app/components/identity/domains/DIDDomainResolverState.ts similarity index 83% rename from client/src/app/components/identity/domains/IdentityDomainResolverState.ts rename to client/src/app/components/identity/domains/DIDDomainResolverState.ts index 7790a85bd..7cd443429 100644 --- a/client/src/app/components/identity/domains/IdentityDomainResolverState.ts +++ b/client/src/app/components/identity/domains/DIDDomainResolverState.ts @@ -4,7 +4,7 @@ export enum Status { Error = "ERROR", } -export interface IdentityDomainResolverState { +export interface DIDDomainResolverState { verifiedDomainsPresentation: Map< string, { diff --git a/client/src/app/components/stardust/address/section/did/DidSection.tsx b/client/src/app/components/stardust/address/section/did/DidSection.tsx index 89dec513e..7e64123fd 100644 --- a/client/src/app/components/stardust/address/section/did/DidSection.tsx +++ b/client/src/app/components/stardust/address/section/did/DidSection.tsx @@ -1,9 +1,9 @@ import React from "react"; -import IdentityStardustResolver from "../../../../identity/IdentityStardustResolver"; -import { IIdentityStardustResolveResponse } from "~/models/api/IIdentityStardustResolveResponse"; +import IdentityStardustResolver from "../../../../identity/DIDResolver"; +import { IDIDResolverResponse } from "~/models/api/IDIDResolverResponse"; interface DIDSectionProps { - resolvedDID: IIdentityStardustResolveResponse | null; + resolvedDID: IDIDResolverResponse | null; network: string; } 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/block/BlockTangleState.scss b/client/src/app/components/stardust/block/BlockTangleState.scss index ce8ae18e1..45984e72b 100644 --- a/client/src/app/components/stardust/block/BlockTangleState.scss +++ b/client/src/app/components/stardust/block/BlockTangleState.scss @@ -22,7 +22,7 @@ font-weight: 500; letter-spacing: 0.5px; - @include phone-down { + @include tablet-down { margin-left: 8px; flex-direction: column; white-space: normal; diff --git a/client/src/app/components/stardust/block/BlockTangleState.tsx b/client/src/app/components/stardust/block/BlockTangleState.tsx index 7b77ad341..35f1d8949 100644 --- a/client/src/app/components/stardust/block/BlockTangleState.tsx +++ b/client/src/app/components/stardust/block/BlockTangleState.tsx @@ -1,19 +1,20 @@ import classNames from "classnames"; -import moment from "moment"; import React, { useEffect, useState } from "react"; import { BlockTangleStateProps } from "./BlockTangleStateProps"; import { useMilestoneDetails } from "~helpers/hooks/useMilestoneDetails"; import Tooltip from "../../Tooltip"; import "./BlockTangleState.scss"; +import { DateHelper } from "~/helpers/dateHelper"; const BlockTangleState: React.FC = ({ network, status, milestoneIndex, hasConflicts, conflictReason, onClick }) => { - const [ago, setAgo] = useState(); + const [milestoneTimestamp, setMilestoneTimestamp] = useState(); const [blockId, setBlockId] = useState(); const [milestoneDetails] = useMilestoneDetails(network, milestoneIndex ?? null); useEffect(() => { if (milestoneDetails?.milestone?.timestamp) { - setAgo(moment(milestoneDetails.milestone?.timestamp * 1000).fromNow()); + const readableTimestamp = DateHelper.format(DateHelper.milliseconds(milestoneDetails.milestone.timestamp)); + setMilestoneTimestamp(readableTimestamp); } setBlockId(milestoneDetails?.blockId); }, [milestoneDetails]); @@ -34,7 +35,7 @@ const BlockTangleState: React.FC = ({ network, status, mi {milestoneIndex && "Confirmed"} {!milestoneIndex && "Pending"} - {milestoneIndex && Created {ago}} + {milestoneIndex && Created {milestoneTimestamp}} )} @@ -77,7 +78,7 @@ const BlockTangleState: React.FC = ({ network, status, mi > Milestone {milestoneIndex} - {ago} + - {milestoneTimestamp} ) : ( "" 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..0098943aa 100644 --- a/client/src/app/components/stardust/history/TransactionCard.tsx +++ b/client/src/app/components/stardust/history/TransactionCard.tsx @@ -1,57 +1,41 @@ import classNames from "classnames"; -import moment from "moment"; -import React, { useContext } from "react"; +import React 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 TruncatedId from "../TruncatedId"; +import TransactionIdView from "./TransactionIdView"; 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
- +
-
-
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..fe95e75bb 100644 --- a/client/src/app/components/stardust/history/TransactionHistory.scss +++ b/client/src/app/components/stardust/history/TransactionHistory.scss @@ -33,19 +33,24 @@ background: var(--transaction-history-dark-row); } + th, + td { + padding: 16px; + } + th { @include font-size(12px); - text-align: center !important; color: $gray-6; font-weight: 600; - text-align: left; + text-align: center; text-transform: uppercase; } td { - padding: 16px 0; text-align: center; + width: 33%; + text-wrap: nowrap; &.transaction-id, &.output-id { @@ -82,12 +87,23 @@ color: var(--amount-color); @include font-size(16px, 21px); font-weight: 700; + text-align: right; &.negative { color: var(--expanded-color); } } } + + td, + th { + &:first-child { + text-align: left; + } + &:last-child { + text-align: right; + } + } } } 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/TransactionIdView.tsx b/client/src/app/components/stardust/history/TransactionIdView.tsx new file mode 100644 index 000000000..7b32149ef --- /dev/null +++ b/client/src/app/components/stardust/history/TransactionIdView.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { STARDUST_SUPPLY_INCREASE_TRANSACTION_ID } from "~/helpers/stardust/transactionsHelper"; +import TruncatedId from "../TruncatedId"; +import Tooltip from "../../Tooltip"; + +export interface ITransactionIdProps { + transactionId: string; + isTransactionFromStardustGenesis: boolean; + transactionLink: string; +} + +const TransactionIdView: React.FC = ({ transactionId, isTransactionFromStardustGenesis, transactionLink }) => { + return ( + <> + {isTransactionFromStardustGenesis && transactionId.includes(STARDUST_SUPPLY_INCREASE_TRANSACTION_ID) ? ( + Stardust Genesis + ) : ( + <> + + {isTransactionFromStardustGenesis && ( + + + warning + + + )} + + )} + + ); +}; + +export default TransactionIdView; diff --git a/client/src/app/components/stardust/history/TransactionRow.tsx b/client/src/app/components/stardust/history/TransactionRow.tsx index 1bc2b6dd5..2bb0e5d47 100644 --- a/client/src/app/components/stardust/history/TransactionRow.tsx +++ b/client/src/app/components/stardust/history/TransactionRow.tsx @@ -1,65 +1,37 @@ import classNames from "classnames"; -import moment from "moment"; -import React, { useContext } from "react"; -import { Link } from "react-router-dom"; +import React 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 Tooltip from "../../Tooltip"; -import TruncatedId from "../TruncatedId"; +import TransactionIdView from "./TransactionIdView"; 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)}`} + setIsFormattedAmounts(!isFormattedAmounts)}> + {balanceChangeFormatted} ); - const isTransactionFromStardustGenesis = - milestoneIndex && TransactionsHelper.isTransactionFromIotaStardustGenesis(network, milestoneIndex); - const transactionLink = isTransactionFromStardustGenesis - ? `/${CHRYSALIS_MAINNET}/search/${transactionId}` - : `/${network}/transaction/${transactionId}`; - return ( - + + {isGenesisByDate ? Genesis : {dateFormatted}} - - - {isTransactionFromStardustGenesis && ( - - - warning - - - )} - - - - - - - {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..a281a7f29 --- /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 { STARDUST_SUPPLY_INCREASE_TRANSACTION_ID, 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 = getTransactionLink(network, transactionId, isTransactionFromStardustGenesis); + + const isSpent = balanceChange <= 0; + + 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, 2, true), + 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); +}; + +export const getTransactionLink = (network: string, transactionId: string, isTransactionFromStardustGenesis: boolean) => { + return isTransactionFromStardustGenesis && !transactionId.includes(STARDUST_SUPPLY_INCREASE_TRANSACTION_ID) + ? `/${CHRYSALIS_MAINNET}/search/${transactionId}` + : `/${network}/transaction/${transactionId}`; +}; diff --git a/client/src/app/routes.tsx b/client/src/app/routes.tsx index 1ad83ecf5..d2db67221 100644 --- a/client/src/app/routes.tsx +++ b/client/src/app/routes.tsx @@ -62,14 +62,21 @@ const buildAppRoutes = (protocolVersion: string, withNetworkContext: (wrappedCom key={keys.next().value} component={(props: RouteComponentProps) => } />, + ]; + + const IdentiyResolverRoute = ( ) => ( )} - />, - ]; + /> + ); + + if (protocolVersion !== STARDUST) { + commonRoutes.push(IdentiyResolverRoute); + } const legacyRoutes = [ { constructor(props: RouteComponentProps & { protocolVersion: string }) { super(props); - - this.state = { - didExample: undefined, - }; } public async componentDidMount(): Promise { super.componentDidMount(); - - this.setDidExample(); } /** @@ -44,10 +36,6 @@ class IdentityResolver extends AsyncComponent<
{!this.props.match.params.did && ( - {this.props.protocolVersion === LEGACY && ( -
This network is not supported!
- )} -

Decentralized Identifier

@@ -61,45 +49,22 @@ class IdentityResolver extends AsyncComponent< network.

- {this.props.protocolVersion !== LEGACY && ( -
- { - this.props.history.push( - `/${this.props.match.params.network}/identity-resolver/${e}`, - ); - }} - network={this.props.match.params.network} - /> -
- )} - - {this.state.didExample && this.props.protocolVersion === STARDUST && ( - - )} + network={this.props.match.params.network} + /> +
)} {this.props.match.params.did && ( -
- {this.props.protocolVersion === LEGACY && ( -
-
This network is not supported!
-
- )} - {this.props.protocolVersion === CHRYSALIS && } -
+
{this.props.protocolVersion === CHRYSALIS && }
)}
@@ -108,16 +73,6 @@ class IdentityResolver extends AsyncComponent<
); } - - private setDidExample() { - const networkService = ServiceFactory.get("network"); - const networks = networkService.networks(); - - const network = networks.find((n) => n.network === this.props.match.params.network); - this.setState({ - didExample: network?.didExample, - }); - } } export default IdentityResolver; diff --git a/client/src/app/routes/stardust/AddressPage.tsx b/client/src/app/routes/stardust/AddressPage.tsx index fee122518..8972379d8 100644 --- a/client/src/app/routes/stardust/AddressPage.tsx +++ b/client/src/app/routes/stardust/AddressPage.tsx @@ -23,7 +23,7 @@ const AddressPage: React.FC> = ({ const { bech32AddressDetails, balance, - sigLockedBalance, + availableBalance, storageRentBalance, isBasicOutputsLoading, isAliasOutputsLoading, @@ -87,7 +87,7 @@ const AddressPage: React.FC> = ({ {balance !== null && ( )} diff --git a/client/src/app/routes/stardust/AddressState.ts b/client/src/app/routes/stardust/AddressState.ts index bc26829d8..19d7ec90b 100644 --- a/client/src/app/routes/stardust/AddressState.ts +++ b/client/src/app/routes/stardust/AddressState.ts @@ -32,12 +32,12 @@ import NetworkContext from "../../context/NetworkContext"; import { AddressRouteProps } from "../AddressRouteProps"; import { useAliasContainsDID } from "~/helpers/hooks/useAliasContainsDID"; import { useResolvedDID } from "~/helpers/hooks/useResolvedDID"; -import { IIdentityStardustResolveResponse } from "~/models/api/IIdentityStardustResolveResponse"; +import { IDIDResolverResponse } from "~/models/api/IDIDResolverResponse"; export interface IAddressState { bech32AddressDetails: IBech32AddressDetails | null; balance: number | null; - sigLockedBalance: number | null; + availableBalance: number | null; storageRentBalance: number | null; addressOutputs: OutputResponse[] | null; addressBasicOutputs: OutputResponse[] | null; @@ -63,13 +63,13 @@ export interface IAddressState { associatedOutputCount: number; aliasContainsDID: boolean; isDIDLoading: boolean; - resolvedDID: IIdentityStardustResolveResponse | null; + resolvedDID: IDIDResolverResponse | null; } const initialState = { bech32AddressDetails: null, balance: null, - sigLockedBalance: null, + availableBalance: null, storageRentBalance: null, addressOutputs: null, addressBasicOutputs: null, @@ -127,7 +127,7 @@ export const useAddressPageState = (): [IAddressState, React.Dispatch> = ({ match: { @@ -51,11 +50,8 @@ const OutputPage: React.FC> = ({ milestoneTimestampBooked, } = outputMetadata ?? {}; - const isTransactionFromStardustGenesis = - milestoneIndexBooked && TransactionsHelper.isTransactionFromIotaStardustGenesis(network, milestoneIndexBooked); - const transctionLink = isTransactionFromStardustGenesis - ? `/${CHRYSALIS_MAINNET}/search/${transactionId}` - : `/${network}/transaction/${transactionId}`; + const isTransactionFromStardustGenesis = TransactionsHelper.isTransactionFromIotaStardustGenesis(network, milestoneIndexBooked ?? 0); + const transactionLink = getTransactionLink(network, transactionId ?? "", isTransactionFromStardustGenesis); return ( (output && ( @@ -101,16 +97,14 @@ const OutputPage: React.FC> = ({ {transactionId && (
Transaction ID
-
- {isTransactionFromStardustGenesis && ( - - warning - +
+ {milestoneIndexBooked && ( + )} -
)} diff --git a/client/src/helpers/hooks/useAddressBalance.ts b/client/src/helpers/hooks/useAddressBalance.ts index f3946e62c..7d647d72a 100644 --- a/client/src/helpers/hooks/useAddressBalance.ts +++ b/client/src/helpers/hooks/useAddressBalance.ts @@ -14,7 +14,7 @@ export function useAddressBalance(network: string, address: string | null): [num const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${STARDUST}`)); const [balance, setBalance] = useState(null); - const [sigLockedBalance, setSigLockedBalance] = useState(null); + const [availableBalance, setAvailableBalance] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -26,14 +26,14 @@ export function useAddressBalance(network: string, address: string | null): [num if (response?.totalBalance !== undefined && isMounted) { setBalance(response.totalBalance); - setSigLockedBalance(response.sigLockedBalance ?? null); + setAvailableBalance(response.availableBalance ?? null); } else if (isMounted) { // Fallback balance from iotajs (node) const addressDetailsWithBalance = await apiClient.addressBalance({ network, address }); if (addressDetailsWithBalance && isMounted) { setBalance(Number(addressDetailsWithBalance.balance)); - setSigLockedBalance(null); + setAvailableBalance(null); } } })(); @@ -42,5 +42,5 @@ export function useAddressBalance(network: string, address: string | null): [num } }, [network, address]); - return [balance, sigLockedBalance, isLoading]; + return [balance, availableBalance, isLoading]; } diff --git a/client/src/helpers/hooks/useAddressHistory.ts b/client/src/helpers/hooks/useAddressHistory.ts index 6ac48f787..1821a6cbe 100644 --- a/client/src/helpers/hooks/useAddressHistory.ts +++ b/client/src/helpers/hooks/useAddressHistory.ts @@ -1,4 +1,3 @@ -import { OutputResponse } from "@iota/sdk-wasm/web"; import { useEffect, useState } from "react"; import { useIsMounted } from "./useIsMounted"; import { ServiceFactory } from "~factories/serviceFactory"; @@ -6,11 +5,23 @@ 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"; +import { OutputResponse } from "@iota/sdk-wasm/web"; + +const OUTPUT_PAGE_SIZE = 10; +const TX_PAGE_SIZE = 10; -interface IOutputDetailsMap { - [outputId: string]: OutputResponse; +const SORT = "newest"; + +interface IHistoryState { + transactionIdToOutputs: Map; + outputsWithDetails: OutputWithDetails[]; + isAddressHistoryLoading: boolean; + cursor: string | undefined; } +export type OutputWithDetails = ITransactionHistoryItem & { details: OutputResponse | null; amount?: string }; + /** * Fetch Address history * @param network The Network in context @@ -22,99 +33,107 @@ 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 [isAddressHistoryLoading, setIsAddressHistoryLoading] = useState(true); - const [cursor, setCursor] = useState(); - const PAGE_SIZE: number = 10; - const SORT: string = "newest"; + const [apiClient] = useState(() => ServiceFactory.get(`api-client-${STARDUST}`)); + const [historyState, setHistoryState] = useState({ + transactionIdToOutputs: new Map(), + outputsWithDetails: [], + isAddressHistoryLoading: true, + cursor: undefined, + }); useEffect(() => { - loadHistory(); - }, [address]); - - 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); - }); - } + if (!address || !isMounted) return; + (async () => { + await loadHistory(); + })(); + }, [address, isMounted]); + + const requestOutputsList = async ( + cursor: string | undefined, + ): Promise<{ outputs: ITransactionHistoryItem[]; cursor: string | undefined }> => { + if (!address) return { outputs: [], cursor: undefined }; + + const request: ITransactionHistoryRequest = { + network, + address, + pageSize: OUTPUT_PAGE_SIZE, + sort: SORT, + cursor, + }; + + const response = (await apiClient.transactionHistory(request)) as ITransactionHistoryResponse | undefined; + const items = response?.items ?? []; + return { + outputs: items, + cursor: response?.cursor, + }; }; - useEffect(() => { - if (history.length > 0) { - 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); + const requestOutputDetails = async (outputId: string): Promise => { + 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; + } + }; + + const loadHistory = async () => { + let { transactionIdToOutputs, outputsWithDetails, cursor } = historyState; + // Search one more than the desired so the incomplete transaction can be removed with .pop() + const targetSize = transactionIdToOutputs.size + TX_PAGE_SIZE + 1; + let searchMore = true; + + setHistoryState((prevState) => ({ ...prevState, isAddressHistoryLoading: true })); - 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); + while (transactionIdToOutputs.size < targetSize && searchMore) { + try { + const { outputs, cursor: newCursor } = await requestOutputsList(cursor); + + if (!newCursor) { + setDisabled?.(true); + searchMore = false; + } + + const fulfilledOutputs: OutputWithDetails[] = await Promise.all( + outputs.map(async (output) => { + const details = await requestOutputDetails(output.outputId); + return { + ...output, + details, + amount: details?.output?.amount, + }; + }), + ); + + outputsWithDetails = [...outputsWithDetails, ...fulfilledOutputs].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; }); + + transactionIdToOutputs = groupOutputsByTransactionId(outputsWithDetails); + cursor = newCursor; + } catch (error) { + console.error("Failed loading transaction history", error); + searchMore = false; + } } - }, [history]); + setHistoryState({ transactionIdToOutputs, outputsWithDetails, isAddressHistoryLoading: false, cursor }); + }; - return [historyView, outputDetailsMap, loadHistory, isAddressHistoryLoading, Boolean(cursor)]; + return [historyState.transactionIdToOutputs, loadHistory, historyState.isAddressHistoryLoading, Boolean(historyState.cursor)]; } diff --git a/client/src/helpers/hooks/useResolvedDID.ts b/client/src/helpers/hooks/useResolvedDID.ts index 6f6bf2a0c..a21c29e32 100644 --- a/client/src/helpers/hooks/useResolvedDID.ts +++ b/client/src/helpers/hooks/useResolvedDID.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useIsMounted } from "./useIsMounted"; import { ServiceFactory } from "~factories/serviceFactory"; import { IdentityService } from "~/services/identityService"; -import { IIdentityStardustResolveResponse } from "~/models/api/IIdentityStardustResolveResponse"; +import { IDIDResolverResponse } from "~/models/api/IDIDResolverResponse"; /** * Fetch resolved DID @@ -11,14 +11,10 @@ import { IIdentityStardustResolveResponse } from "~/models/api/IIdentityStardust * @param addressHex Hex representation of the alias address * @returns The DID response and loading bool. */ -export function useResolvedDID( - network: string, - bech32Hrp: string, - addressHex: string | null, -): [IIdentityStardustResolveResponse | null, boolean] { +export function useResolvedDID(network: string, bech32Hrp: string, addressHex: string | null): [IDIDResolverResponse | null, boolean] { const isMounted = useIsMounted(); const [identityService] = useState(ServiceFactory.get("identity")); - const [identityResponse, setidentityResponse] = useState(null); + const [identityResponse, setidentityResponse] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { diff --git a/client/src/helpers/stardust/transactionsHelper.ts b/client/src/helpers/stardust/transactionsHelper.ts index 8128f49d0..3fd705069 100644 --- a/client/src/helpers/stardust/transactionsHelper.ts +++ b/client/src/helpers/stardust/transactionsHelper.ts @@ -55,6 +55,8 @@ const HEX_PARTICIPATE = "0x5041525449434950415445"; */ export const STARDUST_GENESIS_MILESTONE = 7669900; +export const STARDUST_SUPPLY_INCREASE_TRANSACTION_ID = "0xb191c4bc825ac6983789e50545d5ef07a1d293a98ad974fc9498cb18"; + export class TransactionsHelper { public static async getInputsAndOutputs( block: Block | undefined, diff --git a/client/src/helpers/stardust/valueFormatHelper.spec.ts b/client/src/helpers/stardust/valueFormatHelper.spec.ts index ac47dedf3..e820c2e5a 100644 --- a/client/src/helpers/stardust/valueFormatHelper.spec.ts +++ b/client/src/helpers/stardust/valueFormatHelper.spec.ts @@ -41,6 +41,10 @@ test("formatAmount should format 1 unit with fraction properly", () => { expect(formatAmount(1234567, tokenInfo)).toBe("1.23 IOTA"); }); +test("formatAmount should format 1 unit with trailing decimals properly", () => { + expect(formatAmount(1000000, tokenInfo, false, 2, true)).toBe("1.00 IOTA"); +}); + test("formatAmount should handle edge case from issue 'explorer/issues/822'", () => { expect(formatAmount(1140000, tokenInfo)).toBe("1.14 IOTA"); }); diff --git a/client/src/helpers/stardust/valueFormatHelper.tsx b/client/src/helpers/stardust/valueFormatHelper.tsx index 628a54035..df66be4f3 100644 --- a/client/src/helpers/stardust/valueFormatHelper.tsx +++ b/client/src/helpers/stardust/valueFormatHelper.tsx @@ -16,13 +16,19 @@ const GENESIS_BLOCK_ID = "0x0000000000000000000000000000000000000000000000000000 * @param decimalPlaces The decimal places to show. * @returns The formatted string. */ -export function formatAmount(value: number, tokenInfo: INodeInfoBaseToken, formatFull: boolean = false, decimalPlaces: number = 2): string { +export function formatAmount( + value: number, + tokenInfo: INodeInfoBaseToken, + formatFull: boolean = false, + decimalPlaces: number = 2, + trailingDecimals?: boolean, +): string { if (formatFull) { return `${value} ${tokenInfo.subunit ?? tokenInfo.unit}`; } const baseTokenValue = value / Math.pow(10, tokenInfo.decimals); - const formattedAmount = toFixedNoRound(baseTokenValue, decimalPlaces); + const formattedAmount = toFixedNoRound(baseTokenValue, decimalPlaces, trailingDecimals); // useMetricPrefix is broken cause it passes a float value to formatBest const amount = tokenInfo.useMetricPrefix ? UnitsHelper.formatBest(baseTokenValue) : `${formattedAmount} `; @@ -44,10 +50,12 @@ export function formatNumberWithCommas(value: bigint): string { * @param precision The decimal places to show. * @returns The formatted amount. */ -function toFixedNoRound(value: number, precision: number = 2): string { +function toFixedNoRound(value: number, precision: number = 2, trailingDecimals?: boolean): string { + const defaultDecimals = "0".repeat(precision); const valueString = `${value}`; - const [integer, fraction] = valueString.split("."); - if (!fraction) { + const [integer, fraction = defaultDecimals] = valueString.split("."); + + if (fraction === defaultDecimals && !trailingDecimals) { return valueString; } diff --git a/client/src/models/api/IIdentityStardustResolveRequest.ts b/client/src/models/api/IDIDResolverRequest.ts similarity index 72% rename from client/src/models/api/IIdentityStardustResolveRequest.ts rename to client/src/models/api/IDIDResolverRequest.ts index ca7ab06ac..a10439946 100644 --- a/client/src/models/api/IIdentityStardustResolveRequest.ts +++ b/client/src/models/api/IDIDResolverRequest.ts @@ -1,4 +1,4 @@ -export interface IIdentityStardustResolveRequest { +export interface IDIDResolverRequest { /** * The network to search on. */ diff --git a/client/src/models/api/IIdentityStardustResolveResponse.ts b/client/src/models/api/IDIDResolverResponse.ts similarity index 86% rename from client/src/models/api/IIdentityStardustResolveResponse.ts rename to client/src/models/api/IDIDResolverResponse.ts index 2ff799278..00ce257b4 100644 --- a/client/src/models/api/IIdentityStardustResolveResponse.ts +++ b/client/src/models/api/IDIDResolverResponse.ts @@ -1,6 +1,6 @@ import { IResponse } from "./IResponse"; -export interface IIdentityStardustResolveResponse extends IResponse { +export interface IDIDResolverResponse extends IResponse { /** * The resolved DID Document. */ diff --git a/client/src/models/api/stardust/address/IAddressBalanceResponse.ts b/client/src/models/api/stardust/address/IAddressBalanceResponse.ts index 92fa362e1..4e65c9c7b 100644 --- a/client/src/models/api/stardust/address/IAddressBalanceResponse.ts +++ b/client/src/models/api/stardust/address/IAddressBalanceResponse.ts @@ -9,7 +9,7 @@ export interface IAddressBalanceResponse extends IResponse { /** * The balance of trivialy unlockable outputs with address unlock condition. */ - sigLockedBalance?: number; + availableBalance?: number; /** * The ledger index at which this balance data was valid. diff --git a/client/src/services/identityService.ts b/client/src/services/identityService.ts index 887eea416..4b53e1c32 100644 --- a/client/src/services/identityService.ts +++ b/client/src/services/identityService.ts @@ -4,11 +4,13 @@ import { StardustApiClient } from "./stardust/stardustApiClient"; import { ServiceFactory } from "~factories/serviceFactory"; import { IIdentityDidHistoryResponse } from "~models/api/IIdentityDidHistoryResponse"; import { IIdentityDidResolveResponse } from "~models/api/IIdentityResolveResponse"; -import { IIdentityStardustResolveResponse } from "~models/api/IIdentityStardustResolveResponse"; +import { IDIDResolverResponse } from "~models/api/IDIDResolverResponse"; 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 @@ -59,13 +61,17 @@ export class IdentityService { * @param {string} network network name * @returns Promise */ - public async resolveIdentityStardust(did: string, network: string): Promise { + public async resolveIdentityStardust(did: string, network: string): Promise { const apiClient = ServiceFactory.get(`api-client-${STARDUST}`); const response = await apiClient.didDocument({ did, network }); return response; } 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/client/src/services/stardust/stardustApiClient.ts b/client/src/services/stardust/stardustApiClient.ts index 19e586421..55450d2db 100644 --- a/client/src/services/stardust/stardustApiClient.ts +++ b/client/src/services/stardust/stardustApiClient.ts @@ -1,7 +1,7 @@ import { IOutputsResponse } from "@iota/sdk-wasm/web"; import { FetchHelper } from "~helpers/fetchHelper"; -import { IIdentityStardustResolveRequest } from "~models/api/IIdentityStardustResolveRequest"; -import { IIdentityStardustResolveResponse } from "~models/api/IIdentityStardustResolveResponse"; +import { IDIDResolverRequest } from "~models/api/IDIDResolverRequest"; +import { IDIDResolverResponse } from "~models/api/IDIDResolverResponse"; import { IMilestoneDetailsRequest } from "~models/api/IMilestoneDetailsRequest"; import { INetworkBoundGetRequest } from "~models/api/INetworkBoundGetRequest"; import { IOutputDetailsRequest } from "~models/api/IOutputDetailsRequest"; @@ -340,8 +340,8 @@ export class StardustApiClient extends ApiClient { ); } - public async didDocument(request: IIdentityStardustResolveRequest): Promise { - return this.callApi(`stardust/did/${request.network}/${request.did}/document`, "get"); + public async didDocument(request: IDIDResolverRequest): Promise { + return this.callApi(`stardust/did/${request.network}/${request.did}/document`, "get"); } /** 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'" },