From c22ebdfc5f4c50b209550f9738314ad65ac6675e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20Ha=C3=9F?= Date: Tue, 12 Dec 2023 18:04:49 +0100 Subject: [PATCH] Feat(Identity): Unify Alias and Identity view (#852) --- api/src/utils/stardust/searchExecutor.ts | 27 +- api/src/utils/stardust/searchQueryBuilder.ts | 18 +- client/package-lock.json | 134 +++++- client/package.json | 1 + client/script/postinstall.sh | 3 + .../identity/IdentityStardustResolver.tsx | 416 +++++++++--------- .../identity/IdentityStardustResolverProps.ts | 13 +- .../identity/IdentityStardustResolverState.ts | 22 - .../domains/IdentityDomainResolver.scss | 7 + .../domains/IdentityDomainResolver.tsx | 92 ++++ .../domains/IdentityDomainResolverProps.ts | 3 + .../domains/IdentityDomainResolverState.ts | 12 + .../section/AddressPageTabbedSections.tsx | 24 +- .../address/section/did/DidSection.tsx | 24 + client/src/app/routes/IdentityResolver.tsx | 4 - client/src/app/routes/SearchState.ts | 5 + .../src/app/routes/stardust/AddressState.ts | 17 +- client/src/app/routes/stardust/Search.tsx | 24 +- .../src/assets/modals/stardust/alias/did.json | 11 + .../src/helpers/hooks/useAliasContainsDID.ts | 29 ++ client/src/helpers/hooks/useResolvedDID.ts | 47 ++ .../api/IIdentityStardustResolveResponse.ts | 2 +- client/src/services/identityService.ts | 5 + 23 files changed, 672 insertions(+), 268 deletions(-) delete mode 100644 client/src/app/components/identity/IdentityStardustResolverState.ts create mode 100644 client/src/app/components/identity/domains/IdentityDomainResolver.scss create mode 100644 client/src/app/components/identity/domains/IdentityDomainResolver.tsx create mode 100644 client/src/app/components/identity/domains/IdentityDomainResolverProps.ts create mode 100644 client/src/app/components/identity/domains/IdentityDomainResolverState.ts create mode 100644 client/src/app/components/stardust/address/section/did/DidSection.tsx create mode 100644 client/src/assets/modals/stardust/alias/did.json create mode 100644 client/src/helpers/hooks/useAliasContainsDID.ts create mode 100644 client/src/helpers/hooks/useResolvedDID.ts diff --git a/api/src/utils/stardust/searchExecutor.ts b/api/src/utils/stardust/searchExecutor.ts index 9bdc85cd9..a7a5ff6ec 100644 --- a/api/src/utils/stardust/searchExecutor.ts +++ b/api/src/utils/stardust/searchExecutor.ts @@ -28,11 +28,30 @@ export class SearchExecutor { const searchQuery = this.query; const promises: Promise[] = []; let promisesResult: ISearchResponse | null = null; - if (searchQuery.did) { - return { - did: searchQuery.did - }; + promises.push( + new Promise((resolve, reject) => { + StardustTangleHelper.tryFetchNodeThenPermanode( + searchQuery.aliasId, + "aliasOutputId", + network + ).then( + aliasOutputs => { + if (aliasOutputs) { + promisesResult = { + aliasId: searchQuery.aliasId, + did: searchQuery.did + }; + resolve(); + } else { + reject(new Error("Output (aliasId) not present")); + } + } + ).catch(_ => { + reject(new Error("Output (aliasId) fetch failed")); + }); + }) + ); } if (searchQuery.milestoneIndex) { diff --git a/api/src/utils/stardust/searchQueryBuilder.ts b/api/src/utils/stardust/searchQueryBuilder.ts index 843bc1a5a..b6477dd46 100644 --- a/api/src/utils/stardust/searchQueryBuilder.ts +++ b/api/src/utils/stardust/searchQueryBuilder.ts @@ -128,7 +128,7 @@ export class SearchQueryBuilder { let foundryId: string; let tag: HexEncodedString; - const did = this.queryLower.startsWith("did:iota:") ? this.query : undefined; + const did = this.isDIDString(this.queryLower) ? this.query : undefined; const milestoneIndex = /^\d+$/.test(this.query) ? Number.parseInt(this.query, 10) : undefined; const queryDetails = this.buildQueryDetails(); @@ -194,9 +194,14 @@ export class SearchQueryBuilder { let addressType: number; let isBech32: boolean = false; - const q = this.queryLower; + let q = this.queryLower; const hrp = this.networkBechHrp; + const did = this.isDIDString(this.queryLower) ? this.query : undefined; + if (did) { + q = this.extractHexFromDID(did); + } + if (Bech32Helper.matches(q, hrp)) { isBech32 = true; @@ -243,5 +248,14 @@ export class SearchQueryBuilder { return "NFT"; } } + + private isDIDString(searchString: string): boolean { + return searchString.startsWith("did:iota:"); + } + + private extractHexFromDID(did: string): string { + // cut off the first two chars + return did.slice(Math.max(0, did.lastIndexOf(":") + 3)); + } } diff --git a/client/package-lock.json b/client/package-lock.json index c729c05d7..f49563495 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,6 +14,7 @@ "@fontsource/ibm-plex-mono": "^4.5.12", "@fontsource/material-icons": "^4.5.4", "@iota/crypto.js": "^1.8.6", + "@iota/identity-wasm": "^1.0.0", "@iota/iota.js": "^1.8.6", "@iota/mam-legacy": "github:iotaledger/mam.js#fddc95f60539b9a31a4db1b5b56e0dedb8994883", "@iota/mam.js": "^1.6.2", @@ -1132,6 +1133,22 @@ "url-parse": "^1.4.7" } }, + "node_modules/@iota/identity-wasm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@iota/identity-wasm/-/identity-wasm-1.0.0.tgz", + "integrity": "sha512-gSmDWQM5ae/QN2Apl4egDVvx2tsk2erlCG2a/QIpQK/c6dJ4F4/7LOaV4pLcnqabmbWm34BnqlEiXDVRYtAwsQ==", + "dependencies": { + "@noble/ed25519": "^1.7.3", + "@types/node-fetch": "^2.6.2", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@iota/sdk-wasm": "^1.0.4" + } + }, "node_modules/@iota/iota.js": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@iota/iota.js/-/iota.js-1.8.6.tgz", @@ -1458,6 +1475,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1833,8 +1861,29 @@ "node_modules/@types/node": { "version": "16.18.60", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.60.tgz", - "integrity": "sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==", - "dev": true + "integrity": "sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.3", @@ -2700,6 +2749,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -3325,6 +3379,17 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comment-json": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.3.tgz", @@ -3916,6 +3981,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -11796,6 +11869,16 @@ "url-parse": "^1.4.7" } }, + "@iota/identity-wasm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@iota/identity-wasm/-/identity-wasm-1.0.0.tgz", + "integrity": "sha512-gSmDWQM5ae/QN2Apl4egDVvx2tsk2erlCG2a/QIpQK/c6dJ4F4/7LOaV4pLcnqabmbWm34BnqlEiXDVRYtAwsQ==", + "requires": { + "@noble/ed25519": "^1.7.3", + "@types/node-fetch": "^2.6.2", + "node-fetch": "^2.6.7" + } + }, "@iota/iota.js": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@iota/iota.js/-/iota.js-1.8.6.tgz", @@ -12065,6 +12148,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -12408,8 +12496,28 @@ "@types/node": { "version": "16.18.60", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.60.tgz", - "integrity": "sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==", - "dev": true + "integrity": "sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==" + }, + "@types/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", + "requires": { + "@types/node": "*", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } }, "@types/normalize-package-data": { "version": "2.4.3", @@ -13063,6 +13171,11 @@ "has-symbols": "^1.0.3" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -13536,6 +13649,14 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "comment-json": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.3.tgz", @@ -13987,6 +14108,11 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", diff --git a/client/package.json b/client/package.json index edc270229..58891f12f 100644 --- a/client/package.json +++ b/client/package.json @@ -24,6 +24,7 @@ "@fontsource/ibm-plex-mono": "^4.5.12", "@fontsource/material-icons": "^4.5.4", "@iota/crypto.js": "^1.8.6", + "@iota/identity-wasm": "^1.0.0", "@iota/iota.js": "^1.8.6", "@iota/mam-legacy": "github:iotaledger/mam.js#fddc95f60539b9a31a4db1b5b56e0dedb8994883", "@iota/mam.js": "^1.6.2", diff --git a/client/script/postinstall.sh b/client/script/postinstall.sh index 33d4fa7c1..e7986d9ef 100755 --- a/client/script/postinstall.sh +++ b/client/script/postinstall.sh @@ -8,3 +8,6 @@ mkdir -p "$TARGET" # stardust cp "$NODE_MODULES/@iota/sdk-wasm/web/wasm/iota_sdk_wasm_bg.wasm" "$TARGET/iota_sdk_stardust_wasm_bg.wasm" +# identity +cp "$NODE_MODULES/@iota/identity-wasm/web/identity_wasm_bg.wasm" "$TARGET/identity_wasm_bg.wasm" + diff --git a/client/src/app/components/identity/IdentityStardustResolver.tsx b/client/src/app/components/identity/IdentityStardustResolver.tsx index 1079d50a8..ac675ea2d 100644 --- a/client/src/app/components/identity/IdentityStardustResolver.tsx +++ b/client/src/app/components/identity/IdentityStardustResolver.tsx @@ -1,243 +1,231 @@ -import React, { Fragment, ReactNode } from "react"; -import { RouteComponentProps } from "react-router-dom"; +import * as identity from "@iota/identity-wasm/web"; +import { DomainLinkageConfiguration, EdDSAJwsVerifier, JwtCredentialValidationOptions, JwtDomainLinkageValidator, LinkedDomainService } from "@iota/identity-wasm/web"; +import React, { Fragment, useEffect, useState } from "react"; +import IdentityDomainResolver from "./domains/IdentityDomainResolver"; import { IdentityStardustResolverProps } from "./IdentityStardustResolverProps"; -import { IdentityStardustResolverState } from "./IdentityStardustResolverState"; -import welcomeMessage from "~assets/modals/identity-resolver/welcome.json"; import { ServiceFactory } from "~factories/serviceFactory"; -import { IdentityDiffStorageService } from "~services/identityDiffStorageService"; import { IdentityService } from "~services/identityService"; -import AsyncComponent from "../AsyncComponent"; import "./IdentityStardustResolver.scss"; import CopyButton from "../CopyButton"; import JsonViewer from "../JsonViewer"; -import Modal from "../Modal"; import Spinner from "../Spinner"; +import { IIdentityStardustResolveResponse } from "~/models/api/IIdentityStardustResolveResponse"; -class IdentityStardustResolver extends AsyncComponent< - RouteComponentProps, - IdentityStardustResolverState -> { - /** - * Timer to check to state update. - */ - private _timerId?: NodeJS.Timer; - - constructor(props: RouteComponentProps) { - super(props); - this.state = { - did: props.match.params.did, - aliasId: getAliasId(props.match.params.did ?? ""), - errorMessage: "" - }; - } +const IdentityStardustResolver: React.FC = ({ resolvedDID, network }) => { - public async componentDidMount(): Promise { - super.componentDidMount(); + const [DID, setDID] = useState(""); + const [governorAddress, setGovernorAddress] = useState(""); + const [stateControllerAddress, setStateControllerAddress] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [verifiedDomains, setVerifiedDomains] = useState>>(new Map()); - if (!this.state.did) { + useEffect(() => { + if (!resolvedDID?.document) { return; } - const resolvedIdentity = await ServiceFactory.get("identity").resolveIdentityStardust( - this.state.did, - this.props.match.params.network - ); + setDID(resolvedDID.document.doc.id as string ?? "") + setGovernorAddress(resolvedDID.document?.meta.governorAddress ?? "") + setStateControllerAddress(resolvedDID.document?.meta.stateControllerAddress ?? "") + setErrorMessage(resolvedDID.error ?? "") + + constructVerifiedDomains(resolvedDID).then((newVerifiedDomains) => { + setVerifiedDomains(newVerifiedDomains); + }) + }, [resolvedDID]) + + return ( +
+
DID
+
+
{DID}
+ +
- if (resolvedIdentity.error) { - this.setState({ - errorMessage: resolvedIdentity.error - }); - return; - } + {resolvedDID?.document && + !errorMessage && ( + + - this.setState({ - resolvedIdentity - }); - } + - /** - * The component will unmount so update flag. - */ - public componentWillUnmount(): void { - super.componentWillUnmount(); - if (this._timerId) { - clearTimeout(this._timerId); - this._timerId = undefined; - } - IdentityDiffStorageService.instance.clearAll(); - } +
+
Linked Domains
+ +
+
+ )} - /** - * Render the component. - * @returns The node to render. - */ - public render(): ReactNode { - const network = this.props.match.params.network; - const governorAddress = this.state.resolvedIdentity?.document?.meta.governorAddress; - const stateControllerAddress = this.state.resolvedIdentity?.document?.meta.stateControllerAddress; - return ( -
-
-
-

- Decentralized Identifier -

- -
-
-
-
-
DID
-
-
{this.props.match.params.did}
- -
+
+

DID Document

- {this.state.resolvedIdentity?.document && - !this.state.errorMessage && ( - - - - - - + + {!resolvedDID && !errorMessage && ( + +

Resolving DID ...

+ +
+ )} + + {errorMessage && ( +
+

ಠ_ಠ

+

{errorMessage}

+
+ )} + + {resolvedDID && !errorMessage && ( +
+
-
-
-
-

- DID Document + )} +

+ +
+ {resolvedDID && !errorMessage && ( + +
+

+ Metadata

-
-
- - {!this.state.resolvedIdentity && !this.state.errorMessage && ( - -

Resolving DID ...

- -
- )} - - {this.state.errorMessage && ( -
-

ಠ_ಠ

-

{this.state.errorMessage}

-
- )} - - {this.state.resolvedIdentity && !this.state.errorMessage && ( -
- -
- )} -
-
-
- {this.state.resolvedIdentity && !this.state.errorMessage && ( -
-
-

- Metadata -

-
-
-
- -
-
+
+
- )} -
+ +
+ )}
- ); - } +
+ ); } +// } export default IdentityStardustResolver; -/** - * - * @param did DID - * @returns Alias ID - */ -function getAliasId(did: string): string { - return did.slice(Math.max(0, did.indexOf(":0x") + 1)); -} +async function constructVerifiedDomains(resolvedDID: IIdentityStardustResolveResponse): Promise>> { + + const newVerifiedDomains = new Map>(); + + await ServiceFactory.get("identity").initLibrary(); + + const didDocument = identity.IotaDocument.fromJSON(resolvedDID.document); + // Get the Linked Domain Services from the DID Document. + const linkedDomainServices = didDocument + .service() + .filter(service => LinkedDomainService.isValid(service)) + .map(service => LinkedDomainService.fromService(service)); + + + for (const entry of linkedDomainServices) { + for (const domain of entry.domains()) { + newVerifiedDomains.set(domain, new Promise((resolve, reject) => { + // Note that according to the specs, the DID Configuration resource must exist + // at the origin's root, Well-Known Resource directory. + const configurationUrl = new URL("/.well-known/did-configuration.json", domain); + + return fetch(configurationUrl).then((response) => { + return response.json().then((jsonResponse) => { + + let parsedConfigurationResource; + + try { + + parsedConfigurationResource = DomainLinkageConfiguration.fromJSON(jsonResponse); + + try { + new JwtDomainLinkageValidator(new EdDSAJwsVerifier()).validateLinkage( + didDocument, + parsedConfigurationResource, + domain, + new JwtCredentialValidationOptions() + ); + + // all good + resolve(); + } catch (err) { + + // return the error from the library + reject(err); + } + + } catch (err) { + console.log(err); + reject(new Error(`Domain Linkage credential invalid domain ${domain}`)); + } + + }).catch((err) => { + console.log(err); + reject(new Error(`could not parse configuration from domain ${domain}`)); + }); + }).catch((err) => { + console.log(err); + reject(new Error(`could not fetch configuration from ${domain}, this could be a CORS error`)); + }); + + })); + } + } + + return newVerifiedDomains; +} diff --git a/client/src/app/components/identity/IdentityStardustResolverProps.ts b/client/src/app/components/identity/IdentityStardustResolverProps.ts index daa4634fc..fe3806e4a 100644 --- a/client/src/app/components/identity/IdentityStardustResolverProps.ts +++ b/client/src/app/components/identity/IdentityStardustResolverProps.ts @@ -1,11 +1,8 @@ +import { IIdentityStardustResolveResponse } from "~/models/api/IIdentityStardustResolveResponse"; + export interface IdentityStardustResolverProps { - /** - * The network channel details to lookup. - */ - network: string; + + resolvedDID: IIdentityStardustResolveResponse | null, - /** - * The DID to be resoloved/debugged - */ - did?: string; + network: string, } diff --git a/client/src/app/components/identity/IdentityStardustResolverState.ts b/client/src/app/components/identity/IdentityStardustResolverState.ts deleted file mode 100644 index 4557be815..000000000 --- a/client/src/app/components/identity/IdentityStardustResolverState.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IIdentityStardustResolveResponse } from "~models/api/IIdentityStardustResolveResponse"; -export interface IdentityStardustResolverState { - /** - * DID to be resolved. - */ - did?: string; - - /** - * ID of the Alias Output that contains the DID Document. - */ - aliasId?: string; - - /** - * Error message if resolving fails. - */ - errorMessage: string; - - /** - * The resolved document if resolution succeeds - */ - resolvedIdentity?: IIdentityStardustResolveResponse; -} diff --git a/client/src/app/components/identity/domains/IdentityDomainResolver.scss b/client/src/app/components/identity/domains/IdentityDomainResolver.scss new file mode 100644 index 000000000..9b87f33ef --- /dev/null +++ b/client/src/app/components/identity/domains/IdentityDomainResolver.scss @@ -0,0 +1,7 @@ +.inline-flex { + display: inline-flex; +} + +.flex-wrap-wrap { + flex-wrap: wrap; +} \ No newline at end of file diff --git a/client/src/app/components/identity/domains/IdentityDomainResolver.tsx b/client/src/app/components/identity/domains/IdentityDomainResolver.tsx new file mode 100644 index 000000000..25dd3df60 --- /dev/null +++ b/client/src/app/components/identity/domains/IdentityDomainResolver.tsx @@ -0,0 +1,92 @@ +import React, { ReactNode } from "react"; +import { IoAlertCircle, IoCheckmarkCircle } from "react-icons/io5"; +import { IdentityDomainResolverProps } from "./IdentityDomainResolverProps"; +import { IdentityDomainResolverState, Status } from "./IdentityDomainResolverState"; +import AsyncComponent from "../../AsyncComponent"; +import "./IdentityDomainResolver.scss"; +import Spinner from "../../Spinner"; +import Tooltip from "../../Tooltip"; + + +class IdentityDomainResolver extends AsyncComponent { + constructor(props: IdentityDomainResolverProps) { + super(props); + this.state = { + verifiedDomainsPresentation: new Map() + }; + } + + public async componentDidMount(): Promise { + super.componentDidMount(); + this.computeVerifiedDomainsPresentation(this.props.verifiedDomains); + } + + public componentDidUpdate(prevProps: IdentityDomainResolverProps) { + if (prevProps !== this.props) { + this.computeVerifiedDomainsPresentation(this.props.verifiedDomains); + } + } + + /** + * Render the component. + * @returns The node to render. + */ + public render(): ReactNode { + return ( +
+ {this.state.verifiedDomainsPresentation.size ? [...this.state.verifiedDomainsPresentation.keys()].map(key => ( +
+ +
+ {this.state.verifiedDomainsPresentation.get(key)?.status === Status.InFlight && + } + {this.state.verifiedDomainsPresentation.get(key)?.status === Status.Verified && + } + {this.state.verifiedDomainsPresentation.get(key)?.status === Status.Error && + ( + + + + )} +
+
+ )) :
no linked domains
} +
+ ); + } + + private computeVerifiedDomainsPresentation(verifiedDomains: IdentityDomainResolverProps["verifiedDomains"]) { + const newVerifiedDomainsPresentation: IdentityDomainResolverState["verifiedDomainsPresentation"] = new Map(); + if (verifiedDomains) { + for (const [key, value] of verifiedDomains.entries()) { + newVerifiedDomainsPresentation.set(key, { status: Status.InFlight }); + value.then(() => { + this.setState({ + verifiedDomainsPresentation: new Map(this.state.verifiedDomainsPresentation).set(key, + { status: Status.Verified } + ) + }); + }).catch(err => { + this.setState({ + verifiedDomainsPresentation: new Map(this.state.verifiedDomainsPresentation).set(key, + { status: Status.Error, message: err.message } + ) + }); + }); + } + } + this.setState({ + verifiedDomainsPresentation: newVerifiedDomainsPresentation + }); + } +} +export default IdentityDomainResolver; diff --git a/client/src/app/components/identity/domains/IdentityDomainResolverProps.ts b/client/src/app/components/identity/domains/IdentityDomainResolverProps.ts new file mode 100644 index 000000000..b37158e7b --- /dev/null +++ b/client/src/app/components/identity/domains/IdentityDomainResolverProps.ts @@ -0,0 +1,3 @@ +export interface IdentityDomainResolverProps { + verifiedDomains?: Map>; +} diff --git a/client/src/app/components/identity/domains/IdentityDomainResolverState.ts b/client/src/app/components/identity/domains/IdentityDomainResolverState.ts new file mode 100644 index 000000000..93f562360 --- /dev/null +++ b/client/src/app/components/identity/domains/IdentityDomainResolverState.ts @@ -0,0 +1,12 @@ +export enum Status { + InFlight = "INFLIGHT", + Verified = "VERIFIED", + Error = "ERROR", +} + +export interface IdentityDomainResolverState { + verifiedDomainsPresentation: Map; +} diff --git a/client/src/app/components/stardust/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/stardust/address/section/AddressPageTabbedSections.tsx index 281b318c6..ea925a944 100644 --- a/client/src/app/components/stardust/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/stardust/address/section/AddressPageTabbedSections.tsx @@ -3,6 +3,7 @@ import React from "react"; import AliasFoundriesSection from "./alias/AliasFoundriesSection"; import AliasStateSection from "./alias/AliasStateSection"; import AssociatedOutputs from "./association/AssociatedOutputs"; +import DIDSection from "./did/DidSection"; import AssetsTable from "./native-tokens/AssetsTable"; import NftMetadataSection from "./nft/NftMetadataSection"; import NftSection from "./nft/NftSection"; @@ -12,6 +13,7 @@ import associatedOuputsMessage from "~assets/modals/stardust/address/associated- import addressNftsMessage from "~assets/modals/stardust/address/nfts-in-wallet.json"; import transactionHistoryMessage from "~assets/modals/stardust/address/transaction-history.json"; import foundriesMessage from "~assets/modals/stardust/alias/foundries.json"; +import didMessage from "~assets/modals/stardust/alias/did.json"; import stateMessage from "~assets/modals/stardust/alias/state.json"; import nftMetadataMessage from "~assets/modals/stardust/nft/metadata.json"; import votingMessage from "~assets/modals/stardust/participation/main-header.json"; @@ -30,6 +32,7 @@ enum DEFAULT_TABS { enum ALIAS_TABS { State = "State", Foundries = "Foundries", + DID = "DID" } enum NFT_TABS { @@ -81,7 +84,9 @@ const buildAliasAddressTabsOptions = ( isAliasStateTabDisabled: boolean, isAliasDetailsLoading: boolean, isAliasFoundriesTabDisabled: boolean, - isAliasFoundriesLoading: boolean + isAliasFoundriesLoading: boolean, + isAliasDIDTabDisabled: boolean, + isAliasDIDLoading: boolean, ) => ({ [ALIAS_TABS.State]: { disabled: isAliasStateTabDisabled, @@ -92,6 +97,11 @@ const buildAliasAddressTabsOptions = ( disabled: isAliasFoundriesTabDisabled, isLoading: isAliasFoundriesLoading, infoContent: foundriesMessage + }, + [ALIAS_TABS.DID]: { + disabled: isAliasDIDTabDisabled, + isLoading: isAliasDIDLoading, + infoContent: didMessage } }); @@ -140,7 +150,8 @@ export const AddressPageTabbedSections: React.FC, + ] : null; @@ -227,7 +243,9 @@ export const AddressPageTabbedSections: React.FC = ({ resolvedDID, network }) => { + return ( +
+
+ +
+
+ ); +}; + +export default DIDSection; + diff --git a/client/src/app/routes/IdentityResolver.tsx b/client/src/app/routes/IdentityResolver.tsx index 6c736a5df..c6b9309c4 100644 --- a/client/src/app/routes/IdentityResolver.tsx +++ b/client/src/app/routes/IdentityResolver.tsx @@ -10,7 +10,6 @@ import { NetworkService } from "~services/networkService"; import AsyncComponent from "../components/AsyncComponent"; import IdentityChrysalisResolver from "../components/identity/IdentityChrysalisResolver"; import IdentitySearchInput from "../components/identity/IdentitySearchInput"; -import IdentityStardustResolver from "../components/identity/IdentityStardustResolver"; import Modal from "../components/Modal"; import "./IdentityResolver.scss"; @@ -109,9 +108,6 @@ class IdentityResolver extends AsyncComponent< {this.props.protocolVersion === CHRYSALIS && ( )} - {this.props.protocolVersion === STARDUST && ( - - )}
)}
diff --git a/client/src/app/routes/SearchState.ts b/client/src/app/routes/SearchState.ts index a1d99b9be..ec221b332 100644 --- a/client/src/app/routes/SearchState.ts +++ b/client/src/app/routes/SearchState.ts @@ -31,6 +31,11 @@ export interface SearchState { */ redirect: string; + /** + * Encoded search/query parameter + */ + search?: string; + /** * State to pass to redirected page. */ diff --git a/client/src/app/routes/stardust/AddressState.ts b/client/src/app/routes/stardust/AddressState.ts index fd5d501f4..1de6ce7a8 100644 --- a/client/src/app/routes/stardust/AddressState.ts +++ b/client/src/app/routes/stardust/AddressState.ts @@ -23,6 +23,9 @@ import { IBech32AddressDetails } from "~models/api/IBech32AddressDetails"; import { IParticipation } from "~models/api/stardust/participation/IParticipation"; 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"; export interface IAddressState { bech32AddressDetails: IBech32AddressDetails | null; @@ -51,6 +54,9 @@ export interface IAddressState { tokensCount: number; nftCount: number; associatedOutputCount: number; + aliasContainsDID: boolean; + isDIDLoading: boolean; + resolvedDID: IIdentityStardustResolveResponse | null; } const initialState = { @@ -79,7 +85,10 @@ const initialState = { eventDetails: null, tokensCount: 0, nftCount: 0, - associatedOutputCount: 0 + associatedOutputCount: 0, + aliasContainsDID: false, + isDIDLoading: true, + resolvedDID: null, }; /** @@ -118,6 +127,8 @@ export const useAddressPageState = (): [IAddressState, React.Dispatch { const locationState = location.state as IAddressPageLocationProps; @@ -142,13 +153,13 @@ export const useAddressPageState = (): [IAddressState, React.Dispatch { diff --git a/client/src/app/routes/stardust/Search.tsx b/client/src/app/routes/stardust/Search.tsx index 9c21280bc..b131d5030 100644 --- a/client/src/app/routes/stardust/Search.tsx +++ b/client/src/app/routes/stardust/Search.tsx @@ -52,6 +52,7 @@ class Search extends AsyncComponent, Searc status: "", completion: "", redirect: "", + search: "", invalidError: "" }; } @@ -84,6 +85,7 @@ class Search extends AsyncComponent, Searc return this.state.redirect ? ( @@ -225,6 +227,7 @@ class Search extends AsyncComponent, Searc }); if (response && Object.keys(response).length > 0) { + const routeSearch = new Map(); let route = ""; let routeParam = query; let redirectState = {}; @@ -260,6 +263,9 @@ class Search extends AsyncComponent, Searc addressDetails: aliasAddress }; routeParam = aliasAddress.bech32; + if (response.did) { + routeSearch.set("tab", "DID"); + } } else if (response.foundryId) { route = "foundry"; routeParam = response.foundryId; @@ -276,14 +282,26 @@ class Search extends AsyncComponent, Searc } else if (response.milestone?.blockId) { route = "block"; routeParam = response.milestone?.blockId; - } else if (response.did) { - route = "identity-resolver"; - routeParam = response.did; } + + const getEncodedSearch = () => { + if (routeSearch.size === 0) { + return ""; + } + + const searchParams = new URLSearchParams(); + for (const [key, value] of routeSearch.entries()) { + searchParams.append(key, value); + } + + return `?${searchParams.toString()}`; + }; + this.setState({ status: "", statusBusy: false, redirect: `/${this.props.match.params.network}/${route}/${routeParam}`, + search: getEncodedSearch(), redirectState }); } else { diff --git a/client/src/assets/modals/stardust/alias/did.json b/client/src/assets/modals/stardust/alias/did.json new file mode 100644 index 000000000..d2f227bbb --- /dev/null +++ b/client/src/assets/modals/stardust/alias/did.json @@ -0,0 +1,11 @@ +{ + "title": "Decentralized identifier", + "description": "

Decentralized Identity or Self-Sovereign Identity (SSI) defines a new method for identity management and authentication. It removes the centralized aspects and puts the Identity subject in full control over its own identity. Decentralized identity provides a solution for the increasing amount of database breaches, the lack of trust in any digital setting, and the increasingly difficult to comply with privacy legislation, such as GDPR.

", + "links": [ + { + "label": "Read more", + "href": "https://wiki.iota.org/identity.rs/welcome/", + "isExternal": true + } + ] +} diff --git a/client/src/helpers/hooks/useAliasContainsDID.ts b/client/src/helpers/hooks/useAliasContainsDID.ts new file mode 100644 index 000000000..20c296903 --- /dev/null +++ b/client/src/helpers/hooks/useAliasContainsDID.ts @@ -0,0 +1,29 @@ +import { AliasOutput, hexToBytes } from "@iota/sdk-wasm/web"; +import { useEffect, useState } from "react"; +import { Converter } from "~helpers/stardust/convertUtils"; + +/** + * Determine if an alias contains a DID + * @param alias The alias output to check + * @returns The result. + */ +export function useAliasContainsDID( + alias: AliasOutput | null +): [boolean] { + const [aliasContainsDID, setAliasContainsDID] = useState(false); + + useEffect(() => { + if(alias && alias.stateMetadata && Converter.isHex(alias.stateMetadata, true)) { + const metaDataBytes = hexToBytes(alias.stateMetadata); + // Check if the first three bytes contain "DID" according to specification: https://wiki.iota.org/identity.rs/references/specifications/iota-did-method-spec/#anatomy-of-the-state-metadata + if (metaDataBytes.length >= 3) { + const testBytes = metaDataBytes.slice(0, 3); + const testString = Converter.bytesToUtf8(testBytes); + setAliasContainsDID(testString === "DID"); + } + } + }, [alias]); + + return [aliasContainsDID]; +} + diff --git a/client/src/helpers/hooks/useResolvedDID.ts b/client/src/helpers/hooks/useResolvedDID.ts new file mode 100644 index 000000000..2c9478461 --- /dev/null +++ b/client/src/helpers/hooks/useResolvedDID.ts @@ -0,0 +1,47 @@ +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"; + +/** + * Fetch resolved DID + * @param network The Network in context + * @param bech32Hrp The alias id + * @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 + ] { + const isMounted = useIsMounted(); + const [identityService] = useState(ServiceFactory.get("identity")); + const [identityResponse, setidentityResponse] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (addressHex) { + // eslint-disable-next-line no-void + void (async () => { + identityService.resolveIdentityStardust( + `did:iota:${bech32Hrp}:${addressHex}`, + network + ).then(response => { + if (isMounted) { + setidentityResponse(response); + } + }).then(() => identityService.initLibrary()) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, addressHex]); + + return [identityResponse, isLoading]; +} diff --git a/client/src/models/api/IIdentityStardustResolveResponse.ts b/client/src/models/api/IIdentityStardustResolveResponse.ts index 27ff98cc7..2ff799278 100644 --- a/client/src/models/api/IIdentityStardustResolveResponse.ts +++ b/client/src/models/api/IIdentityStardustResolveResponse.ts @@ -5,7 +5,7 @@ export interface IIdentityStardustResolveResponse extends IResponse { * The resolved DID Document. */ document?: { - doc: unknown; + doc: Record; meta: { governorAddress: string; stateControllerAddress: string; diff --git a/client/src/services/identityService.ts b/client/src/services/identityService.ts index 17ad84ad4..a584ccffc 100644 --- a/client/src/services/identityService.ts +++ b/client/src/services/identityService.ts @@ -6,6 +6,7 @@ import { IIdentityDidHistoryResponse } from "~models/api/IIdentityDidHistoryResp import { IIdentityDidResolveResponse } from "~models/api/IIdentityResolveResponse"; import { IIdentityStardustResolveResponse } from "~models/api/IIdentityStardustResolveResponse"; import { CHRYSALIS, STARDUST } from "~models/config/protocolVersion"; +import * as identity from "@iota/identity-wasm/web"; export class IdentityService { /** @@ -63,4 +64,8 @@ export class IdentityService { const response = await apiClient.didDocument({ did, network }); return response; } + + public async initLibrary(path = "/wasm/identity_wasm_bg.wasm") { + return await identity.init(path); + } }