diff --git a/package.json b/package.json index 22b6c546d462..9bd2e6c0559c 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,11 @@ "test:one": "polkadot-dev-run-test --env browser", "test:skipped": "echo 'tests skipped'" }, + "dependencies": { + "@azns/resolver-core": "^1.4.0", + "@azns/resolver-react": "^1.5.0", + "loglevel": "^1.8.1" + }, "devDependencies": { "@crustio/crust-pin": "^1.0.0", "@pinata/sdk": "^1.2.1", @@ -120,6 +125,7 @@ "@polkadot/x-textdecoder": "^11.1.3", "@polkadot/x-textencoder": "^11.1.3", "@polkadot/x-ws": "^11.1.3", + "@types/react": "^18.2.21", "styled-components": "^5.3.1", "typescript": "^5.0.4" } diff --git a/packages/apps-config/package.json b/packages/apps-config/package.json index 42da2a34aedd..b8cc96f8fd5e 100644 --- a/packages/apps-config/package.json +++ b/packages/apps-config/package.json @@ -16,6 +16,7 @@ "version": "0.127.2-2-x", "main": "index.js", "dependencies": { + "@azns/resolver-core": "^1.4.0", "@polkadot/api": "^10.3.4", "@polkadot/api-derive": "^10.3.4", "@polkadot/networks": "^11.1.3", diff --git a/packages/apps-config/src/links/azeroId.ts b/packages/apps-config/src/links/azeroId.ts new file mode 100644 index 000000000000..f8facc58297b --- /dev/null +++ b/packages/apps-config/src/links/azeroId.ts @@ -0,0 +1,70 @@ +// Copyright 2017-2023 @polkadot/apps-config authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { ExternalDef } from './types.js'; + +import { resolveAddressToDomain, SupportedChainId } from '@azns/resolver-core'; + +import { externalAzeroIdLogoBlackSVG, externalAzeroIdLogoPrimarySVG } from '../ui/logos/external/index.js'; + +async function getSubdomainFromAddress ( + address: string, + options: { + chainId: SupportedChainId.AlephZero| SupportedChainId.AlephZeroTestnet; + customApi?: ApiPromise; + } +) { + try { + const { primaryDomain } = await resolveAddressToDomain(address, options); + const domainParts = primaryDomain?.split('.'); + + return domainParts?.slice(0, -1).join('.') || undefined; + } catch { + return undefined; + } +} + +export const AzeroId: ExternalDef = { + chains: { + 'Aleph Zero': SupportedChainId.AlephZero + }, + create: (_chain, _path, data, _hash, customApi) => { + return getSubdomainFromAddress(data.toString(), { chainId: SupportedChainId.AlephZero, customApi }) + .then((domain) => domain && `https://${domain}.azero.id/`); + }, + homepage: 'https://azero.id/', + isActive: true, + paths: { + address: 'account', + validator: 'validator' + }, + ui: { + logo: { + dark: externalAzeroIdLogoPrimarySVG, + light: externalAzeroIdLogoBlackSVG + } + } +}; + +export const TzeroId: ExternalDef = { + chains: { + 'Aleph Zero Testnet': SupportedChainId.AlephZeroTestnet + }, + create: (_chain, _path, data, _hash, customApi) => { + return getSubdomainFromAddress(data.toString(), { chainId: SupportedChainId.AlephZeroTestnet, customApi }) + .then((domain) => domain && `https://${domain}.tzero.id/`); + }, + homepage: 'https://tzero.id/', + isActive: true, + paths: { + address: 'account', + validator: 'validator' + }, + ui: { + logo: { + dark: externalAzeroIdLogoPrimarySVG, + light: externalAzeroIdLogoBlackSVG + } + } +}; diff --git a/packages/apps-config/src/links/index.ts b/packages/apps-config/src/links/index.ts index 684a31c7b306..e6f84514bc64 100644 --- a/packages/apps-config/src/links/index.ts +++ b/packages/apps-config/src/links/index.ts @@ -3,8 +3,12 @@ import type { ExternalDef } from './types.js'; +import { AzeroId, TzeroId } from './azeroId.js'; import { Subscan } from './subscan.js'; export const externalLinks: Record = { + 'AZERO.ID': AzeroId, + 'TZERO.ID': TzeroId, + // eslint-disable-next-line sort-keys Subscan }; diff --git a/packages/apps-config/src/links/subscan.ts b/packages/apps-config/src/links/subscan.ts index 8ec25239159a..2965c9df7c88 100644 --- a/packages/apps-config/src/links/subscan.ts +++ b/packages/apps-config/src/links/subscan.ts @@ -30,6 +30,9 @@ export const Subscan: ExternalDef = { validator: 'validator' }, ui: { - logo: externalSubscanPNG + logo: { + dark: externalSubscanPNG, + light: externalSubscanPNG + } } }; diff --git a/packages/apps-config/src/links/types.ts b/packages/apps-config/src/links/types.ts index 96791d168ae1..d5d2e6cad467 100644 --- a/packages/apps-config/src/links/types.ts +++ b/packages/apps-config/src/links/types.ts @@ -3,6 +3,8 @@ import type { BN } from '@polkadot/util'; +import { ApiPromise } from '@polkadot/api'; + export interface LinkPath { // general address?: string; @@ -33,7 +35,18 @@ export interface ExternalDef { homepage: string; isActive: boolean; paths: LinkPath; - ui: { logo: string; } + ui: { + logo: { + dark: string; + light: string; + }; + }; - create: (chain: string, path: string, data: BN | number | string, hash?: string) => string; + create: ( + chain: string, + path: string, + data: BN | number | string, + hash: string | undefined, + api: ApiPromise | undefined, + ) => string | Promise; } diff --git a/packages/apps-config/src/ui/logos/external/azeroIdLogoBlack.svg b/packages/apps-config/src/ui/logos/external/azeroIdLogoBlack.svg new file mode 100644 index 000000000000..31b8553297de --- /dev/null +++ b/packages/apps-config/src/ui/logos/external/azeroIdLogoBlack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/apps-config/src/ui/logos/external/azeroIdLogoGrey.svg b/packages/apps-config/src/ui/logos/external/azeroIdLogoGrey.svg new file mode 100644 index 000000000000..901045b03a39 --- /dev/null +++ b/packages/apps-config/src/ui/logos/external/azeroIdLogoGrey.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/apps-config/src/ui/logos/external/azeroIdLogoPrimary.svg b/packages/apps-config/src/ui/logos/external/azeroIdLogoPrimary.svg new file mode 100644 index 000000000000..1606e7d6c311 --- /dev/null +++ b/packages/apps-config/src/ui/logos/external/azeroIdLogoPrimary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoBlackSVG.ts b/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoBlackSVG.ts new file mode 100644 index 000000000000..307dbfdc768d --- /dev/null +++ b/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoBlackSVG.ts @@ -0,0 +1,6 @@ +// Copyright 2017-2023 @polkadot/apps authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Do not edit. Auto-generated via node scripts/imgConvert.mjs + +export const externalAzeroIdLogoBlackSVG = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PGcgZmlsbD0iIzE4MTkxNyIgY2xpcC1wYXRoPSJ1cmwoI2EpIj48cGF0aCBkPSJNMjU2LjUzNyA0MzcuMDU0QzE3MS42ODcgNDM3LjA1NCAxMDAuNTQ5IDM3Ny42ODUgODEuNjc3MSAyOTguMThDODEuMTYxMSAyOTYuMTA3IDc5LjMxODIgMjk0LjU1MyA3Ny4xODAzIDI5NC41NTNIMTEuNzE4NEM4Ljg0MzM0IDI5NC41NTMgNi41NTgwNyAyOTcuMjE4IDcuMDc0MSAzMDAuMTA1QzI3Ljc4OSA0MTkuNTg0IDEzMS42NTggNTEwLjc4NCAyNTYuNTM3IDUxMC43ODRDMzgxLjQxNiA1MTAuNzg0IDQ4NS4yODYgNDE5LjU4NCA1MDYgMzAwLjEwNUM1MDYuNTE2IDI5Ny4yMTggNTA0LjIzMSAyOTQuNTUzIDUwMS4zNTYgMjk0LjU1M0g0MzUuODk0QzQzMy43NTYgMjk0LjU1MyA0MzEuOTEzIDI5Ni4wMzMgNDMxLjM5NyAyOTguMThDNDEyLjUyNSAzNzcuNjg1IDM0MS4zODcgNDM3LjA1NCAyNTYuNTM3IDQzNy4wNTRaIi8+PHBhdGggZD0iTTI1Ni41MzcgNzUuNzMwNEMzNDEuMzg3IDc1LjczMDQgNDEyLjUyNSAxMzUuMSA0MzEuMzk3IDIxNC42MDRDNDMxLjkxMyAyMTYuNjc3IDQzMy43NTYgMjE4LjIzMSA0MzUuODk0IDIxOC4yMzFINTAxLjM1NkM1MDQuMjMxIDIxOC4yMzEgNTA2LjUxNiAyMTUuNTY3IDUwNiAyMTIuNjc5QzQ4NS4yMTEgOTMuMjAwNyAzODEuMzQyIDIgMjU2LjUzNyAyQzEzMS43MzEgMiAyNy43ODg1IDkzLjIwMDcgNi45OTk4OCAyMTIuNjc5QzYuNDgzODUgMjE1LjU2NyA4Ljc2OTEyIDIxOC4yMzEgMTEuNjQ0MSAyMTguMjMxSDc3LjE3OThDNzkuMzE3NyAyMTguMjMxIDgxLjE2MDYgMjE2Ljc1MSA4MS42NzY2IDIxNC42MDRDMTAwLjU0OSAxMzUuMSAxNzEuNjg3IDc1LjczMDQgMjU2LjUzNyA3NS43MzA0WiIvPjxwYXRoIGQ9Ik0yNTYuNTM3IDIwNC42ODVDMjQwLjMxOSAyMDQuNjg1IDIyNS43OTYgMjEyLjIzNSAyMTYuMzYgMjI0LjA4QzIwOS4yODMgMjMyLjk2MyAyMDUuMDA4IDI0NC4xNDEgMjA1LjAwOCAyNTYuNDI5QzIwNS4wMDggMjY4LjcxOCAyMDkuMjgzIDI3OS44OTYgMjE2LjM2IDI4OC43NzlDMjI1Ljc5NiAzMDAuNTQ5IDI0MC4yNDUgMzA4LjE3NCAyNTYuNTM3IDMwOC4xNzRDMjcyLjgyOSAzMDguMTc0IDI4Ny4yNzggMzAwLjYyMyAyOTYuNzE0IDI4OC43NzlDMzAzLjc5IDI3OS44OTYgMzA4LjA2NiAyNjguNzE4IDMwOC4wNjYgMjU2LjQyOUMzMDguMDY2IDI0NC4xNDEgMzAzLjc5IDIzMi45NjMgMjk2LjcxNCAyMjQuMDhDMjg3LjI3OCAyMTIuMzA5IDI3Mi44MjkgMjA0LjY4NSAyNTYuNTM3IDIwNC42ODVaIi8+PC9nPjxkZWZzPjxjbGlwUGF0aCBpZD0iYSI+PHJlY3Qgd2lkdGg9IjQ5OSIgaGVpZ2h0PSI1MDguNzg0IiBmaWxsPSIjZmZmIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3IDIpIi8+PC9jbGlwUGF0aD48L2RlZnM+PC9zdmc+'; diff --git a/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoGreySVG.ts b/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoGreySVG.ts new file mode 100644 index 000000000000..984911ec0071 --- /dev/null +++ b/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoGreySVG.ts @@ -0,0 +1,6 @@ +// Copyright 2017-2023 @polkadot/apps authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Do not edit. Auto-generated via node scripts/imgConvert.mjs + +export const externalAzeroIdLogoGreySVG = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTMiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMyAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPG1hc2sgaWQ9Im1hc2swXzFfMzI0IiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjEzIiBoZWlnaHQ9IjEzIj4KPHBhdGggZD0iTTEyLjUyNDcgMC4zMTAwNTlIMC43NTU0OTNWMTIuMzEwMUgxMi41MjQ3VjAuMzEwMDU5WiIgZmlsbD0id2hpdGUiLz4KPC9tYXNrPgo8ZyBtYXNrPSJ1cmwoI21hc2swXzFfMzI0KSI+CjxwYXRoIGQ9Ik02LjY0MDk5IDEwLjU3MTJDNC42Mzk3NSAxMC41NzEyIDIuOTYxOTEgOS4xNzA5MyAyLjUxNjgxIDcuMjk1NzVDMi41MDQ2NCA3LjI0Njg2IDIuNDYxMTcgNy4yMTAyMSAyLjQxMDc1IDcuMjEwMjFIMC44NjY3ODdDMC43OTg5NzcgNy4yMTAyMSAwLjc0NTA3NyA3LjI3MzA2IDAuNzU3MjQ4IDcuMzQxMTVDMS4yNDU4MiAxMC4xNTkxIDMuNjk1NjQgMTIuMzEwMiA2LjY0MDk5IDEyLjMxMDJDOS41ODYzNCAxMi4zMTAyIDEyLjAzNjIgMTAuMTU5MSAxMi41MjQ3IDcuMzQxMTVDMTIuNTM2OSA3LjI3MzA2IDEyLjQ4MyA3LjIxMDIxIDEyLjQxNTIgNy4yMTAyMUgxMC44NzEyQzEwLjgyMDggNy4yMTAyMSAxMC43NzczIDcuMjQ1MTEgMTAuNzY1MiA3LjI5NTc1QzEwLjMyMDEgOS4xNzA5MyA4LjY0MjIzIDEwLjU3MTIgNi42NDA5OSAxMC41NzEyWiIgZmlsbD0iI0JDQkJCQSIvPgo8cGF0aCBkPSJNNi42NDAxMyAyLjA0OTA0QzguNjQxMzggMi4wNDkwNCAxMC4zMTkyIDMuNDQ5MzEgMTAuNzY0MyA1LjMyNDQ2QzEwLjc3NjUgNS4zNzMzNSAxMC44MiA1LjQxMDAxIDEwLjg3MDQgNS40MTAwMUgxMi40MTQzQzEyLjQ4MjIgNS40MTAwMSAxMi41MzYxIDUuMzQ3MTcgMTIuNTIzOSA1LjI3OTA2QzEyLjAzMzYgMi40NjEwOSA5LjU4Mzc0IDAuMzEwMDU5IDYuNjQwMTMgMC4zMTAwNTlDMy42OTY1IDAuMzEwMDU5IDEuMjQ0OTUgMi40NjEwOSAwLjc1NDYzOSA1LjI3OTA2QzAuNzQyNDY4IDUuMzQ3MTcgMC43OTYzNjggNS40MTAwMSAwLjg2NDE3NiA1LjQxMDAxSDIuNDA5ODhDMi40NjAzIDUuNDEwMDEgMi41MDM3NyA1LjM3NTEgMi41MTU5NCA1LjMyNDQ2QzIuOTYxMDYgMy40NDkzMSA0LjYzODg5IDIuMDQ5MDQgNi42NDAxMyAyLjA0OTA0WiIgZmlsbD0iI0JDQkJCQSIvPgo8cGF0aCBkPSJNNi42NDA5MSA1LjA5MDU4QzYuMjU4NCA1LjA5MDU4IDUuOTE1ODcgNS4yNjg2NSA1LjY5MzMxIDUuNTQ4MDJDNS41MjY0IDUuNzU3NTMgNS40MjU1NyA2LjAyMTE3IDUuNDI1NTcgNi4zMTA5OUM1LjQyNTU3IDYuNjAwODQgNS41MjY0IDYuODY0NDggNS42OTMzMSA3LjA3Mzk5QzUuOTE1ODcgNy4zNTE1OSA2LjI1NjY1IDcuNTMxNDMgNi42NDA5MSA3LjUzMTQzQzcuMDI1MTcgNy41MzE0MyA3LjM2NTk2IDcuMzUzMzQgNy41ODg1MSA3LjA3Mzk5QzcuNzU1NCA2Ljg2NDQ4IDcuODU2MjYgNi42MDA4NCA3Ljg1NjI2IDYuMzEwOTlDNy44NTYyNiA2LjAyMTE3IDcuNzU1NCA1Ljc1NzUzIDcuNTg4NTEgNS41NDgwMkM3LjM2NTk2IDUuMjcwMzkgNy4wMjUxNyA1LjA5MDU4IDYuNjQwOTEgNS4wOTA1OFoiIGZpbGw9IiNCQ0JCQkEiLz4KPC9nPgo8L3N2Zz4K'; diff --git a/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoPrimarySVG.ts b/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoPrimarySVG.ts new file mode 100644 index 000000000000..78ec7783dd7d --- /dev/null +++ b/packages/apps-config/src/ui/logos/external/generated/azeroIdLogoPrimarySVG.ts @@ -0,0 +1,6 @@ +// Copyright 2017-2023 @polkadot/apps authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Do not edit. Auto-generated via node scripts/imgConvert.mjs + +export const externalAzeroIdLogoPrimarySVG = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PGcgZmlsbD0iI0U2RkQzOSIgY2xpcC1wYXRoPSJ1cmwoI2EpIj48cGF0aCBkPSJNMjU2LjUzNyA0MzcuMDU0QzE3MS42ODcgNDM3LjA1NCAxMDAuNTQ5IDM3Ny42ODUgODEuNjc3MSAyOTguMThDODEuMTYxMSAyOTYuMTA3IDc5LjMxODIgMjk0LjU1MyA3Ny4xODAzIDI5NC41NTNIMTEuNzE4NEM4Ljg0MzM0IDI5NC41NTMgNi41NTgwNyAyOTcuMjE4IDcuMDc0MSAzMDAuMTA1QzI3Ljc4OSA0MTkuNTg0IDEzMS42NTggNTEwLjc4NCAyNTYuNTM3IDUxMC43ODRDMzgxLjQxNiA1MTAuNzg0IDQ4NS4yODYgNDE5LjU4NCA1MDYgMzAwLjEwNUM1MDYuNTE2IDI5Ny4yMTggNTA0LjIzMSAyOTQuNTUzIDUwMS4zNTYgMjk0LjU1M0g0MzUuODk0QzQzMy43NTYgMjk0LjU1MyA0MzEuOTEzIDI5Ni4wMzMgNDMxLjM5NyAyOTguMThDNDEyLjUyNSAzNzcuNjg1IDM0MS4zODcgNDM3LjA1NCAyNTYuNTM3IDQzNy4wNTRaIi8+PHBhdGggZD0iTTI1Ni41MzcgNzUuNzMwNEMzNDEuMzg3IDc1LjczMDQgNDEyLjUyNSAxMzUuMSA0MzEuMzk3IDIxNC42MDRDNDMxLjkxMyAyMTYuNjc3IDQzMy43NTYgMjE4LjIzMSA0MzUuODk0IDIxOC4yMzFINTAxLjM1NkM1MDQuMjMxIDIxOC4yMzEgNTA2LjUxNiAyMTUuNTY3IDUwNiAyMTIuNjc5QzQ4NS4yMTEgOTMuMjAwNyAzODEuMzQyIDIgMjU2LjUzNyAyQzEzMS43MzEgMiAyNy43ODg1IDkzLjIwMDcgNi45OTk4OCAyMTIuNjc5QzYuNDgzODUgMjE1LjU2NyA4Ljc2OTEyIDIxOC4yMzEgMTEuNjQ0MSAyMTguMjMxSDc3LjE3OThDNzkuMzE3NyAyMTguMjMxIDgxLjE2MDYgMjE2Ljc1MSA4MS42NzY2IDIxNC42MDRDMTAwLjU0OSAxMzUuMSAxNzEuNjg3IDc1LjczMDQgMjU2LjUzNyA3NS43MzA0WiIvPjxwYXRoIGQ9Ik0yNTYuNTM3IDIwNC42ODVDMjQwLjMxOSAyMDQuNjg1IDIyNS43OTYgMjEyLjIzNSAyMTYuMzYgMjI0LjA4QzIwOS4yODMgMjMyLjk2MyAyMDUuMDA4IDI0NC4xNDEgMjA1LjAwOCAyNTYuNDI5QzIwNS4wMDggMjY4LjcxOCAyMDkuMjgzIDI3OS44OTYgMjE2LjM2IDI4OC43NzlDMjI1Ljc5NiAzMDAuNTQ5IDI0MC4yNDUgMzA4LjE3NCAyNTYuNTM3IDMwOC4xNzRDMjcyLjgyOSAzMDguMTc0IDI4Ny4yNzggMzAwLjYyMyAyOTYuNzE0IDI4OC43NzlDMzAzLjc5IDI3OS44OTYgMzA4LjA2NiAyNjguNzE4IDMwOC4wNjYgMjU2LjQyOUMzMDguMDY2IDI0NC4xNDEgMzAzLjc5IDIzMi45NjMgMjk2LjcxNCAyMjQuMDhDMjg3LjI3OCAyMTIuMzA5IDI3Mi44MjkgMjA0LjY4NSAyNTYuNTM3IDIwNC42ODVaIi8+PC9nPjxkZWZzPjxjbGlwUGF0aCBpZD0iYSI+PHJlY3Qgd2lkdGg9IjQ5OSIgaGVpZ2h0PSI1MDguNzg0IiBmaWxsPSIjZmZmIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3IDIpIi8+PC9jbGlwUGF0aD48L2RlZnM+PC9zdmc+'; diff --git a/packages/apps-config/src/ui/logos/external/index.ts b/packages/apps-config/src/ui/logos/external/index.ts index fdcf5b173afe..d662cc52f818 100644 --- a/packages/apps-config/src/ui/logos/external/index.ts +++ b/packages/apps-config/src/ui/logos/external/index.ts @@ -3,6 +3,9 @@ // Do not edit. Auto-generated via node scripts/imgConvert.mjs +export { externalAzeroIdLogoBlackSVG } from './generated/azeroIdLogoBlackSVG.js'; +export { externalAzeroIdLogoGreySVG } from './generated/azeroIdLogoGreySVG.js'; +export { externalAzeroIdLogoPrimarySVG } from './generated/azeroIdLogoPrimarySVG.js'; export { externalCommonwealthPNG } from './generated/commonwealthPNG.js'; export { externalDotreasurySVG } from './generated/dotreasurySVG.js'; export { externalDotscannerPNG } from './generated/dotscannerPNG.js'; diff --git a/packages/apps/public/locales/en/app-accounts.json b/packages/apps/public/locales/en/app-accounts.json index 4354909f32e2..9bb2ff2f6c44 100644 --- a/packages/apps/public/locales/en/app-accounts.json +++ b/packages/apps/public/locales/en/app-accounts.json @@ -311,6 +311,7 @@ "send": "send", "send from account": "send from account", "send to address": "send to address", + "send to address or domain": "send to address or domain", "signatories": "signatories", "signatory": "signatory", "somebody@example.com": "somebody@example.com", diff --git a/packages/apps/public/locales/en/app-addresses.json b/packages/apps/public/locales/en/app-addresses.json index a30a29c1013d..9b7c7b7f66d2 100644 --- a/packages/apps/public/locales/en/app-addresses.json +++ b/packages/apps/public/locales/en/app-addresses.json @@ -8,6 +8,7 @@ "address created": "address created", "address edited": "address edited", "address forgotten": "address forgotten", + "address or domain": "address or domain", "contacts": "contacts", "filter by name or tags": "filter by name or tags", "name": "name", diff --git a/packages/apps/public/locales/en/app-staking.json b/packages/apps/public/locales/en/app-staking.json index 36d76cc3c68e..c426a91a923a 100644 --- a/packages/apps/public/locales/en/app-staking.json +++ b/packages/apps/public/locales/en/app-staking.json @@ -260,6 +260,7 @@ "existing/active nominators": "existing/active nominators", "expected block count": "expected block count", "filter by name, address or index": "filter by name, address or index", + "filter by name, address, index or domain": "filter by name, address, index or domain", "finalizing committee size": "finalizing committee size", "first": "first", "generated public key": "generated public key", diff --git a/packages/apps/public/locales/en/react-components.json b/packages/apps/public/locales/en/react-components.json index 490e8d3393af..dbd9abab1170 100644 --- a/packages/apps/public/locales/en/react-components.json +++ b/packages/apps/public/locales/en/react-components.json @@ -1,5 +1,6 @@ { "0.1x voting balance, no lockup period": "0.1x voting balance, no lockup period", + "AZERO.ID Primary Domain": "AZERO.ID Primary Domain", "Cancel": "Cancel", "Confirm account removal": "Confirm account removal", "Confirm address removal": "Confirm address removal", @@ -9,6 +10,7 @@ "No execution details available for this proposal": "No execution details available for this proposal", "Positive number": "Positive number", "Prior locked voting": "Prior locked voting", + "Register on-chain domain": "Register on-chain domain", "Retrieving data": "Retrieving data", "Submit": "Submit", "Tags": "Tags", @@ -24,6 +26,7 @@ "You are about to remove this account from your list of available accounts. Once completed, should you need to access it again, you will have to re-create the account either via seed or via a backup file.": "You are about to remove this account from your list of available accounts. Once completed, should you need to access it again, you will have to re-create the account either via seed or via a backup file.", "You are about to remove this address from your address book. Once completed, should you need to access it again, you will have to re-add the address.": "You are about to remove this address from your address book. Once completed, should you need to access it again, you will have to re-add the address.", "You are about to remove this contract from your list of available contracts. Once completed, should you need to access it again, you will have to manually add the contract's address in the Instantiate tab.": "You are about to remove this contract from your list of available contracts. Once completed, should you need to access it again, you will have to manually add the contract's address in the Instantiate tab.", + "account address copied": "account address copied", "address copied": "address copied", "available to be unlocked": "available to be unlocked", "beneficiary": "beneficiary", @@ -35,9 +38,11 @@ "commission": "commission", "contract event": "contract event", "democracy": "democracy", + "domain copied": "domain copied", "everything": "everything", "external links": "external links", "extrinsic hash": "extrinsic hash", + "filter by name, address, account index or domain": "filter by name, address, account index or domain", "filter by name, address, or account index": "filter by name, address, or account index", "lifetime": "lifetime", "link": "link", diff --git a/packages/apps/public/locales/en/translation.json b/packages/apps/public/locales/en/translation.json index 689f029f4d54..375bfc8b17cf 100644 --- a/packages/apps/public/locales/en/translation.json +++ b/packages/apps/public/locales/en/translation.json @@ -3,6 +3,7 @@ "@yourname:matrix.org": "", "A controller account should not be set to manage multiple stashes. The selected controller is already controlling {{stashId}}": "", "A controller account should not map to another stash. This selected controller is a stash, controlled by {{bondedId}}": "", + "AZERO.ID Primary Domain": "", "Account type {{index}}": "", "Accounts": "", "Active": "", @@ -127,6 +128,7 @@ "Produced blocks": "", "Proposer": "", "Recovery": "", + "Register on-chain domain": "", "Retrieving data": "", "Retrieving nominators": "", "Retrieving validators": "", diff --git a/packages/page-accounts/src/Accounts/Account.tsx b/packages/page-accounts/src/Accounts/Account.tsx index 12998fd710e4..d9d1e3a163e3 100644 --- a/packages/page-accounts/src/Accounts/Account.tsx +++ b/packages/page-accounts/src/Accounts/Account.tsx @@ -1,6 +1,7 @@ // Copyright 2017-2023 @polkadot/app-accounts authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { ApiPromise } from '@polkadot/api'; import type { SubmittableExtrinsic } from '@polkadot/api/types'; import type { DeriveDemocracyLock, DeriveStakingAccount } from '@polkadot/api-derive/types'; import type { Ledger } from '@polkadot/hw-ledger'; @@ -12,7 +13,6 @@ import type { AccountBalance, Delegation } from '../types.js'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { ApiPromise } from '@polkadot/api'; import useAccountLocks from '@polkadot/app-referenda/useAccountLocks'; import { AddressInfo, AddressSmall, Badge, Button, ChainLock, Columar, CryptoType, Forget, LinkExternal, Menu, Popup, styled, Table, Tags } from '@polkadot/react-components'; import { useAccountInfo, useApi, useBalancesAll, useBestNumber, useCall, useLedger, useQueue, useStakingInfo, useToggle } from '@polkadot/react-hooks'; @@ -472,6 +472,9 @@ function Account ({ account: { address, meta }, className = '', delegation, filt /> {t('index')}: {accountIndex} )} +
diff --git a/packages/page-accounts/src/Sidebar/Multisig.tsx b/packages/page-accounts/src/Sidebar/Multisig.tsx index b668908afb38..7de6cc5ae1c9 100644 --- a/packages/page-accounts/src/Sidebar/Multisig.tsx +++ b/packages/page-accounts/src/Sidebar/Multisig.tsx @@ -32,7 +32,7 @@ function Multisig ({ isMultisig, meta }: Props): React.ReactElement | nul
{t('threshold')}
- {threshold}/{(who as string[]).length} + {threshold?.toString()}/{(who as string[]).length}
diff --git a/packages/page-accounts/src/Sidebar/Sidebar.tsx b/packages/page-accounts/src/Sidebar/Sidebar.tsx index 22bc48ae9441..679a9ada56ad 100644 --- a/packages/page-accounts/src/Sidebar/Sidebar.tsx +++ b/packages/page-accounts/src/Sidebar/Sidebar.tsx @@ -149,6 +149,7 @@ const StyledSidebar = styled(Sidebar)` .ui--AddressMenu-index { display: flex; flex-direction: row; + margin-bottom: 0.571rem; label { font-size: var(--font-size-small); diff --git a/packages/page-accounts/src/modals/Create.tsx b/packages/page-accounts/src/modals/Create.tsx index 21441494cc4b..d9b41cb50439 100644 --- a/packages/page-accounts/src/modals/Create.tsx +++ b/packages/page-accounts/src/modals/Create.tsx @@ -253,6 +253,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ @@ -145,7 +146,7 @@ function Import ({ className = '', onClose, onStatusChange }: Props): React.Reac {error && ( )} - {differentGenesis && ( + {!!differentGenesis && ( ('The network from which this account was originally generated is different than the network you are currently connected to. Once imported ensure you toggle the "allow on any network" option for the account to keep it visible on the current network.')} /> )} diff --git a/packages/page-accounts/src/modals/Qr.tsx b/packages/page-accounts/src/modals/Qr.tsx index 067be78426e0..b8ea2eb5ee8b 100644 --- a/packages/page-accounts/src/modals/Qr.tsx +++ b/packages/page-accounts/src/modals/Qr.tsx @@ -129,6 +129,7 @@ function QrModal ({ className = '', onClose, onStatusChange }: Props): React.Rea diff --git a/packages/page-accounts/src/modals/Transfer.tsx b/packages/page-accounts/src/modals/Transfer.tsx index c3a87d1cb53a..0830e93ffdaa 100644 --- a/packages/page-accounts/src/modals/Transfer.tsx +++ b/packages/page-accounts/src/modals/Transfer.tsx @@ -120,7 +120,7 @@ function Transfer ({ className = '', onClose, recipientId: propRecipientId, send ('send to address')} + label={t('send to address or domain')} labelExtra={ ('transferrable')} diff --git a/packages/page-addresses/src/Contacts/Address.tsx b/packages/page-addresses/src/Contacts/Address.tsx index 77634cf01ce2..b243119413d5 100644 --- a/packages/page-addresses/src/Contacts/Address.tsx +++ b/packages/page-addresses/src/Contacts/Address.tsx @@ -185,6 +185,7 @@ function Address ({ address, className = '', filter, isFavorite, toggleFavorite /> diff --git a/packages/page-addresses/src/modals/Create.tsx b/packages/page-addresses/src/modals/Create.tsx index 09f70a9128b9..182db36d682f 100644 --- a/packages/page-addresses/src/modals/Create.tsx +++ b/packages/page-addresses/src/modals/Create.tsx @@ -5,22 +5,20 @@ import type { DeriveAccountInfo } from '@polkadot/api-derive/types'; import type { ActionStatus } from '@polkadot/react-components/Status/types'; import type { ModalProps as Props } from '../types.js'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { AddressRow, Button, Input, InputAddress, Modal } from '@polkadot/react-components'; import { useApi, useCall } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; -import { hexToU8a } from '@polkadot/util'; -import { ethereumEncode } from '@polkadot/util-crypto'; import { useTranslation } from '../translate.js'; +import { getAddressFromDomain, getValidatedAddress } from '../util.js'; interface AddrState { address: string; addressInput: string; isAddressExisting: boolean; isAddressValid: boolean; - isPublicKey: boolean; } interface NameState { @@ -30,51 +28,49 @@ interface NameState { function Create ({ onClose, onStatusChange }: Props): React.ReactElement { const { t } = useTranslation(); - const { api, isEthereum } = useApi(); + const { api, isEthereum, systemChain } = useApi(); const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' }); - const [{ address, addressInput, isAddressExisting, isAddressValid }, setAddress] = useState({ address: '', addressInput: '', isAddressExisting: false, isAddressValid: false, isPublicKey: false }); + const [{ address, addressInput, isAddressExisting, isAddressValid }, setAddress] = useState({ address: '', addressInput: '', isAddressExisting: false, isAddressValid: false }); + const latestAddressInput = useRef(address); const info = useCall(!!address && isAddressValid && api.derive.accounts.info, [address]); const isValid = (isAddressValid && isNameValid) && !!info?.accountId; - const _onChangeAddress = useCallback( - (addressInput: string): void => { - let address = ''; - let isAddressValid = true; - let isAddressExisting = false; - let isPublicKey = false; - - try { - if (isEthereum) { - const rawAddress = hexToU8a(addressInput); + const _onChangeAddressAsync = useCallback( + async (input: string): Promise => { + latestAddressInput.current = input; - address = ethereumEncode(rawAddress); - isPublicKey = rawAddress.length === 20; - } else { - const publicKey = keyring.decodeAddress(addressInput); + let address: string | null | undefined = getValidatedAddress(input, isEthereum); + let isAddressExisting = false; - address = keyring.encodeAddress(publicKey); - isPublicKey = publicKey.length === 32; - } + if (!address) { + address = await getAddressFromDomain(input, { api, systemChain }); + } - if (!isAddressValid) { - const old = keyring.getAddress(address); + if (address) { + const old = keyring.getAddress(address); - if (old) { - const newName = old.meta.name || name; + if (old) { + const newName = old.meta.name || name; - isAddressExisting = true; - isAddressValid = true; + isAddressExisting = true; - setName({ isNameValid: !!(newName || '').trim(), name: newName }); - } + setName({ isNameValid: !!(newName || '').trim(), name: newName }); } - } catch { - isAddressValid = false; } - setAddress({ address: isAddressValid ? address : '', addressInput, isAddressExisting, isAddressValid, isPublicKey }); + if (latestAddressInput.current === input) { + // Prevent possible race condition -- only the latest input can change state. + setAddress({ address: address || '', addressInput: input, isAddressExisting, isAddressValid: !!address }); + } + }, + [isEthereum, name, api, systemChain] + ); + + const _onChangeAddress = useCallback( + (input: string) => { + _onChangeAddressAsync(input).catch(console.error); }, - [isEthereum, name] + [_onChangeAddressAsync] ); const _onChangeName = useCallback( @@ -121,6 +117,7 @@ function Create ({ onClose, onStatusChange }: Props): React.ReactElement autoFocus className='full' isError={!isAddressValid} - label={t('address')} + label={t('address or domain')} onChange={_onChangeAddress} onEnter={_onCommit} placeholder={t('new address')} diff --git a/packages/page-addresses/src/util.tsx b/packages/page-addresses/src/util.tsx index e436cc0aa276..f5f63f9755ae 100644 --- a/packages/page-addresses/src/util.tsx +++ b/packages/page-addresses/src/util.tsx @@ -1,13 +1,18 @@ // Copyright 2017-2023 @polkadot/app-addresses authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { ApiPromise } from '@polkadot/api'; import type { KeyringAddress } from '@polkadot/ui-keyring/types'; import type { SortedAccount } from './types.js'; +import { resolveDomainToAddress } from '@azns/resolver-core'; import React from 'react'; import { Menu } from '@polkadot/react-components'; +import { systemNameToChainId } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; +import { hexToU8a } from '@polkadot/util'; +import { ethereumEncode } from '@polkadot/util-crypto'; export function createMenuGroup (items: (React.ReactNode | false | undefined | null)[]): React.ReactNode | null { const filtered = items.filter((item): item is React.ReactNode => !!item); @@ -63,3 +68,33 @@ export function sortAccounts (addresses: string[], favorites: string[]): SortedA : -1 ); } + +export function getValidatedAddress (address: string, isEthereum: boolean): string | undefined { + try { + if (isEthereum) { + const rawAddress = hexToU8a(address); + + return ethereumEncode(rawAddress); + } + + const publicKey = keyring.decodeAddress(address); + + return keyring.encodeAddress(publicKey); + } catch { + return undefined; + } +} + +export async function getAddressFromDomain (domain: string, { api, systemChain }: {api: ApiPromise, systemChain: string}): Promise { + const chainId = systemNameToChainId.get(systemChain); + + if (!chainId) { + return; + } + + try { + return (await resolveDomainToAddress(domain, { chainId, customApi: api })).address; + } catch { + return undefined; + } +} diff --git a/packages/page-bounties/src/BountyActions/index.tsx b/packages/page-bounties/src/BountyActions/index.tsx index 44d154b0dfce..fa17098cc18e 100644 --- a/packages/page-bounties/src/BountyActions/index.tsx +++ b/packages/page-bounties/src/BountyActions/index.tsx @@ -24,7 +24,7 @@ interface Props { value: Balance; } -export function BountyActions ({ bestNumber, description, fee, index, proposals, status, value }: Props): JSX.Element { +export function BountyActions ({ bestNumber, description, fee, index, proposals, status, value }: Props): React.JSX.Element { const { beneficiary, curator, unlockAt } = useBountyStatus(status); const blocksUntilPayout = useMemo(() => unlockAt?.sub(bestNumber), [bestNumber, unlockAt]); diff --git a/packages/page-bounties/src/BountyInfos/VotingSummary.tsx b/packages/page-bounties/src/BountyInfos/VotingSummary.tsx index 16787547a687..6390f77df84b 100644 --- a/packages/page-bounties/src/BountyInfos/VotingSummary.tsx +++ b/packages/page-bounties/src/BountyInfos/VotingSummary.tsx @@ -19,7 +19,7 @@ interface Props { status: BountyStatus; } -function VotingSummary ({ className, proposal, status }: Props): JSX.Element { +function VotingSummary ({ className, proposal, status }: Props): React.JSX.Element { const { members } = useCollectiveMembers('council'); const { t } = useTranslation(); const ayes = useMemo(() => proposal?.votes?.ayes?.length, [proposal]); diff --git a/packages/page-bounties/src/BountyInfos/index.tsx b/packages/page-bounties/src/BountyInfos/index.tsx index cef2c732756d..674af71610d2 100644 --- a/packages/page-bounties/src/BountyInfos/index.tsx +++ b/packages/page-bounties/src/BountyInfos/index.tsx @@ -19,7 +19,7 @@ interface Props { status: BountyStatus; } -function BountyInfos ({ beneficiary, proposals, status }: Props): JSX.Element { +function BountyInfos ({ beneficiary, proposals, status }: Props): React.JSX.Element { const { t } = useTranslation(); const proposalToDisplay = useMemo(() => proposals && getProposalToDisplay(proposals, status), [proposals, status]); diff --git a/packages/page-bounties/src/BountyNextActionInfo/BountyActionMessage.tsx b/packages/page-bounties/src/BountyNextActionInfo/BountyActionMessage.tsx index 4c3e3c11550a..fd0cb08f77e2 100644 --- a/packages/page-bounties/src/BountyNextActionInfo/BountyActionMessage.tsx +++ b/packages/page-bounties/src/BountyNextActionInfo/BountyActionMessage.tsx @@ -20,7 +20,7 @@ interface Props { export const BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING = 10; const BLOCKS_LEFT_TO_SHOW_WARNING = new BN('10000'); -function BountyActionMessage ({ bestNumber, blocksUntilUpdate, status }: Props): JSX.Element { +function BountyActionMessage ({ bestNumber, blocksUntilUpdate, status }: Props): React.JSX.Element { const { t } = useTranslation(); const { unlockAt } = useBountyStatus(status); const { bountyUpdatePeriod } = useBounties(); diff --git a/packages/page-bounties/src/Description.tsx b/packages/page-bounties/src/Description.tsx index 3173372393b5..529a3a42087e 100644 --- a/packages/page-bounties/src/Description.tsx +++ b/packages/page-bounties/src/Description.tsx @@ -11,7 +11,7 @@ interface Props { description: string; } -function Description ({ className = '', dataTestId = '', description }: Props): JSX.Element { +function Description ({ className = '', dataTestId = '', description }: Props): React.JSX.Element { return ( +
{t('index')} {formatNumber(value.nonce)}
@@ -186,8 +190,7 @@ const StyledTr = styled.tr` .explorer--BlockByHash-nonce { font-size: var(--font-size-small); - margin-left: 2.25rem; - margin-top: -0.5rem; + margin-left: 34px; opacity: var(--opacity-light); text-align: left; } @@ -206,4 +209,9 @@ const StyledTr = styled.tr` } `; +const StyledAzeroId = styled(AzeroId)` + margin-left: 34px; + margin-bottom: 3px; +`; + export default React.memo(ExtrinsicDisplay); diff --git a/packages/page-js/src/snippets/storage-examples.ts b/packages/page-js/src/snippets/storage-examples.ts index cc30937c8a45..4464b0fe7d3d 100644 --- a/packages/page-js/src/snippets/storage-examples.ts +++ b/packages/page-js/src/snippets/storage-examples.ts @@ -7,7 +7,7 @@ const label = { children: 'Storage', color: 'blue', size: 'tiny' -}; +} as const; export const storageGetInfo: Snippet = { code: `// Get chain state information diff --git a/packages/page-parachains/src/Overview/Parachain.tsx b/packages/page-parachains/src/Overview/Parachain.tsx index ef842921e7fb..cbe203bfb9da 100644 --- a/packages/page-parachains/src/Overview/Parachain.tsx +++ b/packages/page-parachains/src/Overview/Parachain.tsx @@ -31,7 +31,7 @@ interface Props { validators?: [GroupIndex, ValidatorInfo[]]; } -function renderAddresses (list?: AccountId[], indices?: BN[]): JSX.Element[] | undefined { +function renderAddresses (list?: AccountId[], indices?: BN[]): React.JSX.Element[] | undefined { return list?.map((id, index) => ( (
('filter by name, address or index')} + label={t('filter by name, address, index or domain')} onChange={_setNameFilter} value={nameFilter} /> diff --git a/packages/page-staking/src/Performance/Address/index.tsx b/packages/page-staking/src/Performance/Address/index.tsx index abfef3fc1820..ee6022c21fb4 100644 --- a/packages/page-staking/src/Performance/Address/index.tsx +++ b/packages/page-staking/src/Performance/Address/index.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useMemo } from 'react'; import { ApiPromise } from '@polkadot/api'; import { AddressSmall, Icon, Spinner } from '@polkadot/react-components'; import { checkVisibility } from '@polkadot/react-components/util'; -import { useApi, useDeriveAccountInfo } from '@polkadot/react-hooks'; +import { useAddressToDomain, useApi, useDeriveAccountInfo } from '@polkadot/react-hooks'; interface Props { address: string; @@ -29,10 +29,11 @@ function queryAddress (address: string) { function Address ({ address, blocksCreated, filterName, rewardPercentage, session }: Props): React.ReactElement | null { const { api } = useApi(); const { accountInfo } = useAddressCalls(api, address); + const { primaryDomain: domain } = useAddressToDomain(address); const isVisible = useMemo( - () => accountInfo ? checkVisibility(api, address, accountInfo, filterName) : true, - [api, accountInfo, address, filterName] + () => accountInfo ? checkVisibility(api, address, { ...accountInfo, domain }, filterName) : true, + [api, accountInfo, address, domain, filterName] ); const onQueryStats = useCallback( diff --git a/packages/page-staking/src/Suspensions/Address/index.tsx b/packages/page-staking/src/Suspensions/Address/index.tsx index 427c02614cc8..3d2bdd457892 100644 --- a/packages/page-staking/src/Suspensions/Address/index.tsx +++ b/packages/page-staking/src/Suspensions/Address/index.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useMemo } from 'react'; import { ApiPromise } from '@polkadot/api'; import { AddressSmall, Icon } from '@polkadot/react-components'; import { checkVisibility } from '@polkadot/react-components/util'; -import { useApi, useDeriveAccountInfo } from '@polkadot/react-hooks'; +import { useAddressToDomain, useApi, useDeriveAccountInfo } from '@polkadot/react-hooks'; interface Props { address: string; @@ -29,6 +29,7 @@ function queryAddress (address: string) { function Address ({ address, era, filterName, suspensionLiftsInEra, suspensionReason }: Props): React.ReactElement | null { const { api } = useApi(); const { accountInfo } = useAddressCalls(api, address); + const { primaryDomain: domain } = useAddressToDomain(address); const onQueryStats = useCallback( () => queryAddress(address), @@ -36,8 +37,8 @@ function Address ({ address, era, filterName, suspensionLiftsInEra, suspensionRe ); const isVisible = useMemo( - () => accountInfo ? checkVisibility(api, address, accountInfo, filterName) : true, - [api, accountInfo, address, filterName] + () => accountInfo ? checkVisibility(api, address, { ...accountInfo, domain }, filterName) : true, + [api, accountInfo, address, domain, filterName] ); if (!isVisible) { diff --git a/packages/page-staking/src/Targets/Validator.tsx b/packages/page-staking/src/Targets/Validator.tsx index 010a5c6d8088..97eeca4a70f2 100644 --- a/packages/page-staking/src/Targets/Validator.tsx +++ b/packages/page-staking/src/Targets/Validator.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { AddressSmall, Badge, Checkbox, Icon, Table } from '@polkadot/react-components'; import { checkVisibility } from '@polkadot/react-components/util'; -import { useApi, useBlockTime, useDeriveAccountInfo } from '@polkadot/react-hooks'; +import { useAddressToDomain, useApi, useBlockTime, useDeriveAccountInfo } from '@polkadot/react-hooks'; import { FormatBalance } from '@polkadot/react-query'; import { formatNumber } from '@polkadot/util'; @@ -37,12 +37,13 @@ function Validator ({ allSlashes, canSelect, filterName, info: { accountId, bond const { api } = useApi(); const accountInfo = useDeriveAccountInfo(accountId); const [,, time] = useBlockTime(lastPayout); + const { primaryDomain: domain } = useAddressToDomain(accountId.toString()); const isVisible = useMemo( () => accountInfo - ? checkVisibility(api, key, accountInfo, filterName) + ? checkVisibility(api, key, { ...accountInfo, domain }, filterName) : true, - [accountInfo, api, filterName, key] + [accountInfo, api, domain, filterName, key] ); const slashes = useMemo( diff --git a/packages/page-staking/src/Validators/Address/index.tsx b/packages/page-staking/src/Validators/Address/index.tsx index 3e37bfed7066..e13feac26bc5 100644 --- a/packages/page-staking/src/Validators/Address/index.tsx +++ b/packages/page-staking/src/Validators/Address/index.tsx @@ -12,7 +12,7 @@ import React, { useMemo } from 'react'; import { ApiPromise } from '@polkadot/api'; import { AddressSmall, Columar, Icon, LinkExternal, Table } from '@polkadot/react-components'; import { checkVisibility } from '@polkadot/react-components/util'; -import { useApi, useCall, useDeriveAccountInfo, useToggle } from '@polkadot/react-hooks'; +import { useAddressToDomain, useApi, useCall, useDeriveAccountInfo, useToggle } from '@polkadot/react-hooks'; import { FormatBalance } from '@polkadot/react-query'; import { BN_ZERO } from '@polkadot/util'; @@ -84,6 +84,7 @@ function Address ({ address, className = '', filterName, hasQueries, isFavorite, const { api } = useApi(); const [isExpanded, toggleIsExpanded] = useToggle(false); const { accountInfo, slashingSpans } = useAddressCalls(api, address); + const { primaryDomain: domain } = useAddressToDomain(address); const { commission, nominators, stakeOther, stakeOwn } = useMemo( () => validatorInfo @@ -93,8 +94,8 @@ function Address ({ address, className = '', filterName, hasQueries, isFavorite, ); const isVisible = useMemo( - () => accountInfo ? checkVisibility(api, address, accountInfo, filterName, withIdentity) : true, - [api, accountInfo, address, filterName, withIdentity] + () => accountInfo ? checkVisibility(api, address, { ...accountInfo, domain }, filterName, withIdentity) : true, + [api, accountInfo, address, domain, filterName, withIdentity] ); const statsLink = useMemo( diff --git a/packages/page-storage/src/Query.tsx b/packages/page-storage/src/Query.tsx index 342776bf1cf7..e724168a55f2 100644 --- a/packages/page-storage/src/Query.tsx +++ b/packages/page-storage/src/Query.tsx @@ -26,7 +26,7 @@ interface Props { interface CacheInstance { Component: React.ComponentType; - render: RenderFn; + render: (createComponent: RenderFn) => React.ComponentType; refresh: (swallowErrors: boolean) => React.ComponentType; } diff --git a/packages/react-components/src/AccountName.tsx b/packages/react-components/src/AccountName.tsx index f20ecacc971a..745bf85ca6fa 100644 --- a/packages/react-components/src/AccountName.tsx +++ b/packages/react-components/src/AccountName.tsx @@ -235,7 +235,7 @@ function AccountName ({ children, className = '', defaultName, label, onClick, o const StyledSpan = styled.span` border: 1px dotted transparent; line-height: 1; - vertical-align: middle; + white-space: nowrap; &.withSidebar:hover { diff --git a/packages/react-components/src/AddressMini.tsx b/packages/react-components/src/AddressMini.tsx index c6b8367ef25d..4e988d3b1472 100644 --- a/packages/react-components/src/AddressMini.tsx +++ b/packages/react-components/src/AddressMini.tsx @@ -72,7 +72,7 @@ function AddressMini ({ balance, bonded, children, className = '', iconInfo, isH {nameExtra} ) - : {value} + : {value.toString()} } )} diff --git a/packages/react-components/src/AddressRow.tsx b/packages/react-components/src/AddressRow.tsx index d3b31ec840cd..fb9c1f67117e 100644 --- a/packages/react-components/src/AddressRow.tsx +++ b/packages/react-components/src/AddressRow.tsx @@ -14,6 +14,7 @@ import Row from './Row.js'; import { styled } from './styled.js'; export interface Props extends RowProps { + isAzeroIdShown?: boolean; isContract?: boolean; isValid?: boolean; fullLength?: boolean; @@ -28,7 +29,7 @@ export interface Props extends RowProps { const DEFAULT_ADDR = '5'.padEnd(48, 'x'); const ICON_SIZE = 32; -function AddressRow ({ buttons, children, className, defaultName, fullLength = false, isContract = false, isDisabled, isEditableName, isInline, isValid: propsIsValid, overlay, value, withTags = false }: Props): React.ReactElement | null { +function AddressRow ({ buttons, children, className, defaultName, fullLength = false, isAzeroIdShown = false, isContract = false, isDisabled, isEditableName, isInline, isValid: propsIsValid, overlay, value, withTags = false }: Props): React.ReactElement | null { const { accountIndex, isNull, name, onSaveName, onSaveTags, setName, setTags, tags } = useAccountInfo(value ? value.toString() : null, isContract); const isValid = !isNull && (propsIsValid || value || accountIndex); @@ -47,6 +48,7 @@ function AddressRow ({ buttons, children, className, defaultName, fullLength = f value={value ? value.toString() : null} /> } + isAzeroIdShown={isAzeroIdShown} isDisabled={isDisabled} isEditableName={isEditableName} isEditableTags diff --git a/packages/react-components/src/AddressSmall.tsx b/packages/react-components/src/AddressSmall.tsx index 0c4e2e29de2c..e36a2084b8b7 100644 --- a/packages/react-components/src/AddressSmall.tsx +++ b/packages/react-components/src/AddressSmall.tsx @@ -3,17 +3,26 @@ import type { AccountId, Address } from '@polkadot/types/interfaces'; -import React from 'react'; +import React, { useCallback } from 'react'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import { useQueue } from '@polkadot/react-hooks'; + +import AzeroId, { AZERO_ID_ROW_HEIGHT } from './AzeroId/index.js'; import IdentityIcon from './IdentityIcon/index.js'; import AccountName from './AccountName.js'; +import Icon from './Icon.js'; import ParentAccount from './ParentAccount.js'; import { styled } from './styled.js'; +import { useTranslation } from './translate.js'; interface Props { children?: React.ReactNode; className?: string; defaultName?: string; + isAzeroIdShown?: boolean; + isParentAddressShown?: boolean; + isRegisterLinkShown?: boolean; onClickName?: () => void; overrideName?: React.ReactNode; parentAddress?: string; @@ -23,94 +32,174 @@ interface Props { value?: string | Address | AccountId | null | Uint8Array; } -function AddressSmall ({ children, className = '', defaultName, onClickName, overrideName, parentAddress, toggle, value, withShortAddress = false, withSidebar = true }: Props): React.ReactElement { +function AddressSmall ({ children, + className = '', + defaultName, + isAzeroIdShown = false, + isParentAddressShown = false, + isRegisterLinkShown = false, + onClickName, + overrideName, + parentAddress, + toggle, + value, + withShortAddress = false, + withSidebar = true }: Props): React.ReactElement { + const { queueAction } = useQueue(); + const { t } = useTranslation(); + + const onCopy = useCallback( + (address: string) => queueAction({ + account: address, + action: t('clipboard'), + message: t('account address copied'), + status: 'queued' + }), + [queueAction, t] + ); + return ( - - - - - - {parentAddress && ( -
- -
- )} - + {isParentAddressShown && ( +
+ {parentAddress && ( + + )} +
+ )} + + + {children} + + {withShortAddress && ( + - {children} -
- {withShortAddress && ( -
- {value} -
- )} -
-
+ + + {value?.toString()} + + + + + )} + {isAzeroIdShown && ( +
+ +
+ )} + ); } -const StyledDiv = styled.div` - overflow-x: hidden; - text-overflow: ellipsis; - white-space: nowrap; +const Container = styled.div` + padding-block: 0.75rem; + + display: grid; + grid-template-columns: max-content 1fr; + grid-template-areas: + " . parentName " + "identityIcon accountName " + " . shortAddress " + " . azeroIdDomain"; + + align-items: center; + column-gap: 0.5rem; - &.withPadding { - padding: 0.75rem 0; + .parentName { + grid-area: parentName; + height: 18px; } - .ui--AddressSmall-icon { - .ui--IdentityIcon { - margin-right: 0.5rem; - vertical-align: middle; - } + .identityIcon { + grid-area: identityIcon; } - .ui--AddressSmall-info { - position: relative; - vertical-align: middle; - - .parentName, .shortAddress { - font-size: var(--font-size-tiny); - } - - .parentName { - left: 0; - position: absolute; - top: -0.80rem; - } - - .shortAddress { - bottom: -0.95rem; - color: #8B8B8B; - display: inline-block; - left: 0; - min-width: var(--width-shortaddr); - max-width: var(--width-shortaddr); - overflow: hidden; - position: absolute; - text-overflow: ellipsis; - } + .accountName { + grid-area: accountName; } - .ui--AccountName { - overflow: hidden; - vertical-align: middle; - white-space: nowrap; + .shortAddress { + grid-area: shortAddress; + height: 18px; + } - &.withSidebar { - cursor: help; - } + .azeroIdDomain { + grid-area: azeroIdDomain; + height: ${AZERO_ID_ROW_HEIGHT}; } `; +const StyledParentAccount = styled(ParentAccount)` + font-size: var(--font-size-small); +`; + +const StyledIdentityIcon = styled(IdentityIcon)` + width: 26px; + height: 26px; + flex-shrink: 0; +`; + +const StyleAccountName = styled(AccountName)` + overflow: hidden; + text-overflow: ellipsis; +`; + +const AddressContainer = styled.button` + display: flex; + align-items: center; + + justify-self: start; + + background-color: inherit; + color: inherit; + padding: 0; + border: unset; + + cursor: copy; + + margin-bottom: 0.25rem; + color: #8B8B8B; +`; + +const ShortAddress = styled.p` + margin: 0; + + font-size: var(--font-size-small); + + display: inline-block; + width: var(--width-shortaddr); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +const SmallIcon = styled(Icon)` + width: 10px; + height: 10px; +`; + export default React.memo(AddressSmall); diff --git a/packages/react-components/src/AddressToggle.tsx b/packages/react-components/src/AddressToggle.tsx index 1037905dbfef..a40c771523ca 100644 --- a/packages/react-components/src/AddressToggle.tsx +++ b/packages/react-components/src/AddressToggle.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useMemo } from 'react'; -import { useApi, useDeriveAccountInfo } from '@polkadot/react-hooks'; +import { useAddressToDomain, useApi, useDeriveAccountInfo } from '@polkadot/react-hooks'; import { checkVisibility } from './util/index.js'; import AddressMini from './AddressMini.js'; @@ -23,10 +23,11 @@ interface Props { function AddressToggle ({ address, className = '', filter, isHidden, noToggle, onChange, value }: Props): React.ReactElement | null { const { api } = useApi(); const info = useDeriveAccountInfo(address); + const domain = useAddressToDomain(address)?.primaryDomain; const isVisible = useMemo( - () => info ? checkVisibility(api, address, info, filter, false) : true, - [api, address, filter, info] + () => info ? checkVisibility(api, address, { ...info, domain }, filter, false) : true, + [api, address, filter, info, domain] ); const _onClick = useCallback( diff --git a/packages/react-components/src/AzeroId/Atoms.tsx b/packages/react-components/src/AzeroId/Atoms.tsx new file mode 100644 index 000000000000..7dc331a99d1b --- /dev/null +++ b/packages/react-components/src/AzeroId/Atoms.tsx @@ -0,0 +1,192 @@ +// Copyright 2017-2023 @polkadot/app-storage authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SupportedChainId } from '@azns/resolver-core'; +import React, { useCallback, useId } from 'react'; +import CopyToClipboard from 'react-copy-to-clipboard'; + +import { externalAzeroIdLogoBlackSVG, externalAzeroIdLogoGreySVG, externalAzeroIdLogoPrimarySVG } from '@polkadot/apps-config/ui/logos/external'; +import { useQueue, useTheme } from '@polkadot/react-hooks'; + +import Icon from '../Icon.js'; +import { styled } from '../styled.js'; +import Tooltip from '../Tooltip.js'; +import { useTranslation } from '../translate.js'; + +type AzeroIdInteractiveDomainProps = { + className?: string; + domain: string; + chainId: SupportedChainId.AlephZero | SupportedChainId.AlephZeroTestnet; +}; + +export const AzeroIdInteractiveDomain = ({ chainId, className, domain }: AzeroIdInteractiveDomainProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const { queueAction } = useQueue(); + + const tooltipId = useId(); + + const onCopy = useCallback( + () => queueAction({ + action: t('clipboard'), + message: t('domain copied'), + status: 'queued' + }), + [queueAction, t] + ); + + const href = { + [SupportedChainId.AlephZero]: `https://azero.id/id/${domain}`, + [SupportedChainId.AlephZeroTestnet]: `https://tzero.id/id/${domain}` + }[chainId]; + + return ( + + + + {t('AZERO.ID Primary Domain')}
} + trigger={tooltipId} + /> + + + + + + + + ); +}; + +type AzeroIdDomainProps = { + className?: string; + domain: string; + isLogoShown?: boolean; + isCopyShown?: boolean; +}; + +export const AzeroIdDomain = ({ className, domain, isCopyShown = false, isLogoShown = true }: AzeroIdDomainProps) => { + const theme = useTheme(); + + return ( + + {isLogoShown && ( + + )} + + {domain.split(/(?=\.)/).map((domainPart, index) => {domainPart})} + {isCopyShown && } + + + ); +}; + +const REGISTER_LINKS = { + [SupportedChainId.AlephZero]: 'https://azero.id/', + [SupportedChainId.AlephZeroTestnet]: 'https://tzero.id/' +} as const; + +type AzeroIdRegisterLinkProps = { + className?: string; + chainId: keyof typeof REGISTER_LINKS; +}; + +export const AzeroIdRegisterLink = ({ chainId, className }: AzeroIdRegisterLinkProps) => { + const { t } = useTranslation(); + + return ( + + + + {t('Register on-chain domain')} + + + ); +}; + +export const AzeroIdPlaceholder = ({ className = '' }: {className?: string}) => ( + +); + +export const AZERO_ID_ROW_HEIGHT = '16px'; + +const Placeholder = styled.p` + width: 160px; + height: ${AZERO_ID_ROW_HEIGHT}; +`; + +const Container = styled.p` + display: flex; + align-items: center; + + margin: 0; + + color: #8B8B8B; + font-size: var(--font-size-small); +`; + +const StyledLink = styled.a` + display: flex; + align-items: center; +`; + +const Logo = styled.img` + width: 16px; + height: ${AZERO_ID_ROW_HEIGHT}; + margin-right: 5px; +`; + +const ClickableText = styled.button` + text-align: left; + background-color: inherit; + color: inherit; + padding: 0; + border: unset; + + + cursor: copy; +`; + +const DomainContainer = styled.div` + word-break: break-word; + + display: flex; + align-items: center; +`; + +const SmallIcon = styled(Icon)` + width: 10px; + height: 10px; + + margin-left: 5px; +`; diff --git a/packages/react-components/src/AzeroId/AzeroId.tsx b/packages/react-components/src/AzeroId/AzeroId.tsx new file mode 100644 index 000000000000..d161b3b92f74 --- /dev/null +++ b/packages/react-components/src/AzeroId/AzeroId.tsx @@ -0,0 +1,69 @@ +// Copyright 2017-2023 @polkadot/app-storage authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SupportedChainId } from '@azns/resolver-core'; +import React from 'react'; + +import { systemNameToChainId, useAddressToDomain, useApi } from '@polkadot/react-hooks'; + +import { AzeroIdInteractiveDomain, AzeroIdPlaceholder, AzeroIdRegisterLink } from './Atoms.js'; + +type WrappedAzeroIdProps = { + address?: string; + className?: string; + isRegisterLinkShown?: boolean; +}; + +type AzeroIdProps = WrappedAzeroIdProps & { + chainId: SupportedChainId.AlephZero | SupportedChainId.AlephZeroTestnet, +}; + +const AzeroId = ({ address, chainId, className, isRegisterLinkShown }: AzeroIdProps) => { + const { hasError, isLoading, primaryDomain } = useAddressToDomain(address); + + if (primaryDomain) { + return ( + + ); + } + + if (!isRegisterLinkShown) { + return null; + } + + if (isLoading || hasError) { + return ; + } + + return ( + + ); +}; + +const WrappedAzeroId = ({ address, className, isRegisterLinkShown = true }: WrappedAzeroIdProps) => { + const { systemChain } = useApi(); + + const chainId = systemNameToChainId.get(systemChain); + + if (!chainId) { + return null; + } + + return ( + + ); +}; + +export default WrappedAzeroId; diff --git a/packages/react-components/src/AzeroId/index.ts b/packages/react-components/src/AzeroId/index.ts new file mode 100644 index 000000000000..f0a3e219a233 --- /dev/null +++ b/packages/react-components/src/AzeroId/index.ts @@ -0,0 +1,5 @@ +// Copyright 2017-2023 @polkadot/app-storage authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { AzeroIdDomain, AzeroIdInteractiveDomain, AzeroIdRegisterLink, AZERO_ID_ROW_HEIGHT } from './Atoms.js'; +export { default } from './AzeroId.js'; diff --git a/packages/react-components/src/InputAddress/KeyPair.tsx b/packages/react-components/src/InputAddress/KeyPair.tsx index e74b109bca36..eca6abcce117 100644 --- a/packages/react-components/src/InputAddress/KeyPair.tsx +++ b/packages/react-components/src/InputAddress/KeyPair.tsx @@ -4,18 +4,20 @@ import React from 'react'; import AccountName from '../AccountName.js'; +import { AzeroIdDomain } from '../AzeroId/index.js'; import IdentityIcon from '../IdentityIcon/index.js'; import { styled } from '../styled.js'; interface Props { address: string; className?: string; + domain?: string | null; isUppercase: boolean; name: string; style?: Record; } -function KeyPair ({ address, className = '' }: Props): React.ReactElement { +function KeyPair ({ address, className = '', domain }: Props): React.ReactElement { return (
+ {domain && }
{address}
@@ -71,4 +74,9 @@ const StyledDiv = styled.div` } `; +const StyledAzeroIdDomain = styled(AzeroIdDomain)` + opacity: var(--opacity-light); + font-size: var(--font-size-small); +`; + export default React.memo(KeyPair); diff --git a/packages/react-components/src/InputAddress/createItem.tsx b/packages/react-components/src/InputAddress/createItem.tsx index 84c67b8014de..0f4e08087d87 100644 --- a/packages/react-components/src/InputAddress/createItem.tsx +++ b/packages/react-components/src/InputAddress/createItem.tsx @@ -11,7 +11,7 @@ import { decodeAddress } from '@polkadot/util-crypto'; import KeyPair from './KeyPair.js'; -export default function createItem (option: KeyringSectionOption, isUppercase = true): Option | null { +export default function createItem (option: KeyringSectionOption & {domain?: string | null}, isUppercase = true): Option | null { const allowedLength = keyring.keyring.type === 'ethereum' ? 20 : 32; @@ -23,6 +23,7 @@ export default function createItem (option: KeyringSectionOption, isUppercase = text: ( diff --git a/packages/react-components/src/InputAddress/index.tsx b/packages/react-components/src/InputAddress/index.tsx index fa139c2b7ef5..ab90774b1892 100644 --- a/packages/react-components/src/InputAddress/index.tsx +++ b/packages/react-components/src/InputAddress/index.tsx @@ -2,13 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 import type { DropdownItemProps } from 'semantic-ui-react'; +import type { ApiPromise } from '@polkadot/api'; import type { KeyringOption$Type, KeyringOptions, KeyringSectionOption, KeyringSectionOptions } from '@polkadot/ui-keyring/options/types'; import type { Option } from './types.js'; -import React from 'react'; +import { resolveDomainToAddress } from '@azns/resolver-core'; +import React, { GetDerivedStateFromProps } from 'react'; import store from 'store'; +import { ApiCtx } from '@polkadot/react-api'; import { withMulti, withObservable } from '@polkadot/react-api/hoc'; +import { systemNameToChainId } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { createOptionItem } from '@polkadot/ui-keyring/options/item'; import { isNull, isUndefined } from '@polkadot/util'; @@ -20,8 +24,10 @@ import { styled } from '../styled.js'; import { getAddressName, toAddress } from '../util/index.js'; import createHeader from './createHeader.js'; import createItem from './createItem.js'; +import wrapWithAddressResolver from './wrapWithAddressResolver.js'; -interface Props { +type Props = { + addressToDomain: Record; className?: string; defaultValue?: Uint8Array | string | null; filter?: string[] | null; @@ -35,16 +41,16 @@ interface Props { onChange?: (value: string | null) => void; onChangeMulti?: (value: string[]) => void; options?: KeyringSectionOption[] | null; - optionsAll?: Record; + optionsAll?: KeyringOptions; placeholder?: string; type?: KeyringOption$Type; value?: string | Uint8Array | string[] | null; withEllipsis?: boolean; withExclude?: boolean; withLabel?: boolean; -} +}; -type ExportedType = React.ComponentType & { +type ExportedType = React.ComponentType> & { createOption: (option: KeyringSectionOption, isUppercase?: boolean) => Option | null; setLastValue: (type: KeyringOption$Type, value: string) => void; }; @@ -52,6 +58,10 @@ type ExportedType = React.ComponentType & { interface State { lastValue?: string; value?: string | string[]; + // Required, because apart from primary domain resolved by HOC, + // addresses have also non-primary domain which we can't query. + // This array allows to keep resolved pairs for search to work. + addressToDomains: Record; } const STORAGE_KEY = 'options:InputAddress'; @@ -80,7 +90,29 @@ function transformToAccountId (value: string): string | null { : accountId; } -function createOption (address: string): Option | null { +async function transformOrResolveToAccountId (addressOrDomain: string, { api, systemChain }: {api: ApiPromise, systemChain: string}): Promise { + const accountId = transformToAccountId(addressOrDomain); + + if (accountId) { + return accountId; + } + + const chainId = systemNameToChainId.get(systemChain); + + if (!chainId) { + return null; + } + + try { + const { address } = await resolveDomainToAddress(addressOrDomain, { chainId, customApi: api }); + + return address ? transformToAccountId(address) : null; + } catch { + return null; + } +} + +function createOption (address: string, domain: string | null | undefined): Option | null { let isRecent: boolean | undefined; const pair = keyring.getAccount(address); let name: string | undefined; @@ -98,7 +130,7 @@ function createOption (address: string): Option | null { } } - return createItem(createOptionItem(address, name), !isRecent); + return createItem({ ...createOptionItem(address, name), domain }, !isRecent); } function readOptions (): Record> { @@ -134,23 +166,33 @@ function dedupe (options: Option[]): Option[] { } class InputAddress extends React.PureComponent { - public override state: State = {}; + static override contextType = ApiCtx; + override context!: React.ContextType; - public static getDerivedStateFromProps ({ type, value }: Props, { lastValue }: State): Pick | null { + public override state: State = { + addressToDomains: {} + }; + + public static getDerivedStateFromProps: GetDerivedStateFromProps = ({ type, value }, { addressToDomains, lastValue }) => { try { return { + addressToDomains, lastValue: lastValue || getLastValue(type), value: Array.isArray(value) - ? value.map((v) => toAddress(v)) + ? value.flatMap((v) => { + const result = toAddress(v); + + return result ? [result] : []; + }) : (toAddress(value) || undefined) }; } catch { return null; } - } + }; public override render (): React.ReactNode { - const { className = '', defaultValue, hideAddress = false, isDisabled = false, isError, isMultiple, label, labelExtra, options, optionsAll, placeholder, type = DEFAULT_TYPE, withEllipsis, withLabel } = this.props; + const { addressToDomain, className = '', defaultValue, hideAddress = false, isDisabled = false, isError, isMultiple, label, labelExtra, options, optionsAll, placeholder, type = DEFAULT_TYPE, withEllipsis, withLabel } = this.props; const hasOptions = (options && options.length !== 0) || (optionsAll && Object.keys(optionsAll[type]).length !== 0); // the options could be delayed, don't render without @@ -162,7 +204,7 @@ class InputAddress extends React.PureComponent { className={className} label={label} > - No accounts are available for selection. + No accounts are available for selection. ); } @@ -179,14 +221,16 @@ class InputAddress extends React.PureComponent { const actualOptions: Option[] = options ? dedupe( options - .map((o) => createItem(o)) + .map((o) => createItem({ ...o, domain: addressToDomain[o.value || ''] })) .filter((o): o is Option => !!o) ) : isDisabled && actualValue - ? [createOption(actualValue)].filter((o): o is Option => !!o) + ? [createOption(actualValue, addressToDomain[actualValue])].filter((o): o is Option => !!o) : actualValue ? this.addActual(actualValue) : this.getFiltered(); + const preparedActualOptions = actualOptions.map(({ value, ...rest }) => ({ ...rest, value: value ?? undefined })); + const _defaultValue = (isMultiple || !isUndefined(value)) ? undefined : actualValue; @@ -206,7 +250,7 @@ class InputAddress extends React.PureComponent { : this.onChange } onSearch={this.onSearch} - options={actualOptions} + options={preparedActualOptions} placeholder={placeholder} renderLabel={ isMultiple @@ -229,7 +273,7 @@ class InputAddress extends React.PureComponent { return this.hasValue(actualValue) ? base - : base.concat(...[createOption(actualValue)].filter((o): o is Option => !!o)); + : base.concat(...[createOption(actualValue, this.props.addressToDomain[actualValue])].filter((o): o is Option => !!o)); } private renderLabel = ({ value }: KeyringSectionOption): React.ReactNode => { @@ -255,11 +299,20 @@ class InputAddress extends React.PureComponent { } private getFiltered (): Option[] { - const { filter, optionsAll, type = DEFAULT_TYPE, withExclude = false } = this.props; + const { addressToDomain, filter, optionsAll, type = DEFAULT_TYPE, withExclude = false } = this.props; + const preparedOptionsPairs = Object.entries(optionsAll || {}).map(([type, options]) => [ + type, + options.map( + (option) => option.value === null + ? createHeader(option) + : createItem({ ...option, domain: addressToDomain[option.value] }) + ) + ] as [keyof KeyringOptions, Option[]]); + const typeToOptions = Object.fromEntries(preparedOptionsPairs); return !optionsAll ? [] - : dedupe(optionsAll[type]).filter(({ value }) => + : dedupe(typeToOptions[type]).filter(({ value }) => !filter || ( !!value && ( withExclude @@ -267,7 +320,7 @@ class InputAddress extends React.PureComponent { : filter.includes(value) ) ) - ); + ).map((option) => ({ ...option, domain: this.props.addressToDomain[option.value || ''] })); } private onChange = (address: string): void => { @@ -295,13 +348,15 @@ class InputAddress extends React.PureComponent { }; private onSearch = (filteredOptions: KeyringSectionOptions, _query: string): DropdownItemProps[] => { - const { isInput = true } = this.props; + const { addressToDomain, isInput = true } = this.props; const query = _query.trim(); const queryLower = query.toLowerCase(); const matches = filteredOptions.filter((item): boolean => !!item.value && ( (item.name.toLowerCase && item.name.toLowerCase().includes(queryLower)) || - item.value.toLowerCase().includes(queryLower) + item.value.toLowerCase().includes(queryLower) || + !!addressToDomain[item.value]?.toLowerCase().includes(queryLower) || + !!this.state.addressToDomains[item.value]?.some((domain) => domain.toLowerCase().includes(queryLower)) ) ); @@ -315,6 +370,30 @@ class InputAddress extends React.PureComponent { ).option ); } + + const { api, systemChain } = this.context; + + transformOrResolveToAccountId(query, { api, systemChain }).then((address) => { + if (!address) { + return; + } + + keyring.saveRecent(address.toString()); + + if (this.state.addressToDomains[address]?.includes(query)) { + return; + } + + this.setState(({ addressToDomains }) => ({ + addressToDomains: { + ...addressToDomains, + [address]: [...new Set([ + ...(addressToDomains[address] || []), + query + ])] + } + })); + }).catch(console.error); } // FIXME The return here is _very_ suspect, but it actually does exactly @@ -376,22 +455,8 @@ const StyledDropdown = styled(Dropdown)` `; const ExportedComponent = withMulti( - InputAddress, - withObservable(keyring.keyringOption.optionsSubject, { - propName: 'optionsAll', - transform: (optionsAll: KeyringOptions): Record => - Object.entries(optionsAll).reduce((result: Record, [type, options]): Record => { - result[type] = options - .map((option): Option | React.ReactNode | null => - option.value === null - ? createHeader(option) - : createItem(option) - ) - .filter((o): o is Option | React.ReactNode => !!o); - - return result; - }, {}) - }) + wrapWithAddressResolver(InputAddress), + withObservable(keyring.keyringOption.optionsSubject, { propName: 'optionsAll' }) ) as ExportedType; ExportedComponent.createOption = createItem; diff --git a/packages/react-components/src/InputAddress/wrapWithAddressResolver.tsx b/packages/react-components/src/InputAddress/wrapWithAddressResolver.tsx new file mode 100644 index 000000000000..fdbbff3d40d1 --- /dev/null +++ b/packages/react-components/src/InputAddress/wrapWithAddressResolver.tsx @@ -0,0 +1,64 @@ +// Copyright 2017-2023 @polkadot/app-storage authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { resolveAddressToDomain } from '@azns/resolver-core'; +import React, { ComponentType, useContext, useEffect, useState } from 'react'; + +import { ApiCtx } from '@polkadot/react-api'; +import { systemNameToChainId } from '@polkadot/react-hooks'; +import { KeyringOptions, KeyringSectionOptions } from '@polkadot/ui-keyring/options/types'; + +type RequiredProps = { + options?: KeyringSectionOptions | null; + optionsAll?: KeyringOptions; +}; + +const wrapWithAddressResolver = (Component: ComponentType): ComponentType> => { + const Wrapped = (props: Props) => { + const [addressToDomain, setAddressToDomain] = useState>({}); + const { api, systemChain } = useContext(ApiCtx); + + const { options, optionsAll } = props; + + useEffect(() => { + const chainId = systemNameToChainId.get(systemChain); + + if (!chainId) { + return; + } + + const allAddressesWithDuplicates = [...(options || []), ...(optionsAll?.allPlus || [])].flatMap(({ value }) => value ? [value] : []); + const allAddresses = [...new Set(allAddressesWithDuplicates)]; + + const unresolvedAddresses = allAddresses.filter((address) => !(address in addressToDomain)); + const domainPromises = unresolvedAddresses.map((address) => resolveAddressToDomain(address, { chainId, customApi: api })); + + if (!domainPromises.length) { + return; + } + + Promise.all(domainPromises).then( + (results) => { + const addressDomainTuples = results.flatMap( + ({ error, primaryDomain }, index) => error + ? [] + : [[unresolvedAddresses[index], primaryDomain] as [string, string | undefined | null]] + ); + + setAddressToDomain({ ...addressToDomain, ...Object.fromEntries(addressDomainTuples) }); + } + ).catch(console.error); + }, [addressToDomain, api, options, optionsAll, systemChain]); + + return ( + + ); + }; + + return Wrapped as ComponentType>; +}; + +export default wrapWithAddressResolver; diff --git a/packages/react-components/src/InputAddressMulti/index.tsx b/packages/react-components/src/InputAddressMulti/index.tsx index 580cda70ea81..3688a79f525e 100644 --- a/packages/react-components/src/InputAddressMulti/index.tsx +++ b/packages/react-components/src/InputAddressMulti/index.tsx @@ -66,7 +66,7 @@ function InputAddressMulti ({ available, availableLabel, className = '', default className='ui--InputAddressMulti-Input' isSmall onChange={setFilter} - placeholder={t('filter by name, address, or account index')} + placeholder={t('filter by name, address, account index or domain')} value={_filter} withLabel={false} /> diff --git a/packages/react-components/src/InputAddressSimple.tsx b/packages/react-components/src/InputAddressSimple.tsx index 32382f016ae0..601bfdad8fa6 100644 --- a/packages/react-components/src/InputAddressSimple.tsx +++ b/packages/react-components/src/InputAddressSimple.tsx @@ -1,8 +1,11 @@ // Copyright 2017-2023 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { resolveDomainToAddress } from '@azns/resolver-core'; import React, { useCallback, useState } from 'react'; +import { systemNameToChainId, useApi } from '@polkadot/react-hooks'; + import IdentityIcon from './IdentityIcon/index.js'; import { toAddress } from './util/index.js'; import Input from './Input.js'; @@ -29,19 +32,37 @@ interface Props { function InputAddressSimple ({ autoFocus, bytesLength, children, className = '', defaultValue, forceIconType, isDisabled, isError, isFull, label, noConvert, onChange, onEnter, onEscape, placeholder }: Props): React.ReactElement { const [address, setAddress] = useState(defaultValue || null); + const { api, systemChain } = useApi(); + + const _prepareInput = useCallback( + async (input: string): Promise => { + const formattedAddress = toAddress(input, undefined, bytesLength) || null; + + if (formattedAddress) { + return noConvert ? input : formattedAddress; + } + + const chainId = systemNameToChainId.get(systemChain); + + if (!chainId) { + return null; + } + + const { address: resolvedAddress } = await resolveDomainToAddress(input, { chainId, customApi: api }); + + return resolvedAddress || null; + }, + [api, bytesLength, noConvert, systemChain] + ); + const _onChange = useCallback( (_address: string): void => { - const address = toAddress(_address, undefined, bytesLength) || null; - const output = noConvert - ? address - ? _address - : null - : address; - - setAddress(output); - onChange && onChange(output); + _prepareInput(_address).catch(() => null).then((output) => { + setAddress(output); + onChange && onChange(output); + }).catch(console.error); }, - [bytesLength, noConvert, onChange] + [_prepareInput, onChange] ); return ( diff --git a/packages/react-components/src/LinkExternal.tsx b/packages/react-components/src/LinkExternal.tsx index 4178856ca78d..f18a69ee4b38 100644 --- a/packages/react-components/src/LinkExternal.tsx +++ b/packages/react-components/src/LinkExternal.tsx @@ -1,18 +1,19 @@ // Copyright 2017-2023 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { ApiPromise } from '@polkadot/api'; import type { LinkTypes } from '@polkadot/apps-config/links/types'; import type { BN } from '@polkadot/util'; -import React, { useMemo } from 'react'; +import React, { useEffect, useState } from 'react'; import { externalLinks } from '@polkadot/apps-config'; -import { useApi } from '@polkadot/react-hooks'; +import { useApi, useTheme } from '@polkadot/react-hooks'; import { styled } from './styled.js'; import { useTranslation } from './translate.js'; -interface Props { +type Props = { className?: string; data: BN | number | string; hash?: string; @@ -21,12 +22,24 @@ interface Props { isSmall?: boolean; type: LinkTypes; withTitle?: boolean; +}; + +type GetLinksOptions = { + api: ApiPromise; + data: BN | number | string; + hash?: string; + isText?: boolean; + themeType: 'dark' | 'light'; + type: LinkTypes; } -function genLinks (systemChain: string, { data, hash, isText, type }: Props): React.ReactNode[] { - return Object +async function genLinks ( + systemChain: string, + { api, data, hash, isText, themeType, type }: GetLinksOptions +): Promise { + const linksPromises = Object .entries(externalLinks) - .map(([name, { chains, create, homepage, isActive, paths, ui }]): React.ReactNode | null => { + .map(async ([name, { chains, create, homepage, isActive, paths, ui }]): Promise => { const extChain = chains[systemChain]; const extPath = paths[type]; @@ -34,9 +47,15 @@ function genLinks (systemChain: string, { data, hash, isText, type }: Props): Re return null; } + const href = await create(extChain, extPath, data, hash, api); + + if (!href) { + return null; + } + return ( {isText ? name - : + : } ); - }) - .filter((node): node is React.ReactNode => !!node); + }); + + try { + const linksOrNulls = await Promise.all(linksPromises); + + return linksOrNulls.filter((node): node is React.ReactElement => !!node); + } catch { + return []; + } } function LinkExternal ({ className = '', data, hash, isSidebar, isSmall, isText, type, withTitle }: Props): React.ReactElement | null { const { t } = useTranslation(); - const { systemChain } = useApi(); + const theme = useTheme(); + const { api, systemChain } = useApi(); + const [links, setLinks] = useState(); - const links = useMemo( - () => genLinks(systemChain, { data, hash, isSidebar, isText, type }), - [systemChain, data, hash, isSidebar, isText, type] - ); + useEffect(() => { + genLinks(systemChain, { api, data, hash, isText, themeType: theme.theme, type }) + .then(setLinks) + .catch(console.error); + }, [api, systemChain, data, hash, isText, theme.theme, type]); - if (!links.length && !withTitle) { + if (!links?.length && !withTitle) { return null; } @@ -72,7 +101,7 @@ function LinkExternal ({ className = '', data, hash, isSidebar, isSmall, isText,
{t('external links')}
)}
- {links.length + {links?.length ? links.map((link, index) => {link}) :
{t('none')}
} diff --git a/packages/react-components/src/Row.tsx b/packages/react-components/src/Row.tsx index 9ee759c10ba7..320011768e7a 100644 --- a/packages/react-components/src/Row.tsx +++ b/packages/react-components/src/Row.tsx @@ -8,6 +8,7 @@ import React, { useCallback } from 'react'; import { useToggle } from '@polkadot/react-hooks'; import EditButton from './EditButton.js'; +import { AzeroId } from './index.js'; import Input from './Input.js'; import { styled } from './styled.js'; import Tags from './Tags.js'; @@ -21,6 +22,7 @@ export interface RowProps { details?: React.ReactNode; icon?: React.ReactNode; iconInfo?: React.ReactNode; + isAzeroIdShown?: boolean; isDisabled?: boolean; isInline?: boolean; isEditableName?: boolean; @@ -34,7 +36,7 @@ export interface RowProps { tags?: string[]; } -function Row ({ address, buttons, children, className = '', defaultName, details, icon, iconInfo, isDisabled, isEditableName, isEditableTags, isInline, isShortAddr = true, name, onChangeName, onChangeTags, onSaveName, onSaveTags, tags }: RowProps): React.ReactElement { +function Row ({ address, buttons, children, className = '', defaultName, details, icon, iconInfo, isAzeroIdShown = false, isDisabled, isEditableName, isEditableTags, isInline, isShortAddr = true, name, onChangeName, onChangeTags, onSaveName, onSaveTags, tags }: RowProps): React.ReactElement { const [isEditingName, toggleIsEditingName] = useToggle(); const [isEditingTags, toggleIsEditingTags] = useToggle(); @@ -86,9 +88,15 @@ function Row ({ address, buttons, children, className = '', defaultName, details )} {address && (
- {address} + {address.toString()}
)} + {isAzeroIdShown && ( + + )} {details} {tags && ( void; onClick: () => void; - options: unknown[]; + options: (React.ReactNode | DropdownItemProps)[]; sortDirection: 'descending' | 'ascending'; } diff --git a/packages/react-components/src/Static.tsx b/packages/react-components/src/Static.tsx index 58f4480f7c5a..3fa4867b1675 100644 --- a/packages/react-components/src/Static.tsx +++ b/packages/react-components/src/Static.tsx @@ -33,11 +33,11 @@ function Static ({ children, className = '', copyValue, defaultValue, isFull, is withLabel={withLabel} >
- {value || defaultValue} + {(value || defaultValue)?.toString()} {children}
{withCopy && ( - + )} ); diff --git a/packages/react-components/src/Status/index.tsx b/packages/react-components/src/Status/index.tsx index 184401199185..f2eeef793c69 100644 --- a/packages/react-components/src/Status/index.tsx +++ b/packages/react-components/src/Status/index.tsx @@ -138,7 +138,7 @@ function renderItem ({ error, extrinsic, id, removeItem, rpc, status }: QueueTx) {section}.{method}
- {error ? (error.message || error) : status} + {error ? (error.message || error).toString() : status}
diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 7b2ccb496cbf..8ce96a20d813 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -12,6 +12,7 @@ export { default as AddressSmall } from './AddressSmall.js'; export { default as AddressToggle } from './AddressToggle.js'; export { default as Available } from './Available.js'; export { default as AvatarItem } from './AvatarItem.js'; +export { default as AzeroId, AzeroIdDomain } from './AzeroId/index.js'; export { default as Badge } from './Badge.js'; export { default as Balance } from './Balance.js'; export { default as BatchWarning } from './BatchWarning.js'; diff --git a/packages/react-components/src/util/checkVisibility.tsx b/packages/react-components/src/util/checkVisibility.tsx index d121cddc2a07..1e03ba12ec62 100644 --- a/packages/react-components/src/util/checkVisibility.tsx +++ b/packages/react-components/src/util/checkVisibility.tsx @@ -7,14 +7,19 @@ import { ApiPromise } from '@polkadot/api'; import { keyring } from '@polkadot/ui-keyring'; import { isFunction } from '@polkadot/util'; -export function checkVisibility (api: ApiPromise, address: string, accountInfo: DeriveAccountInfo, filterName = '', onlyNamed = false): boolean { +export function checkVisibility (api: ApiPromise, address: string, accountInfo: DeriveAccountInfo & {domain?: string | null}, filterName = '', onlyNamed = false): boolean { let isVisible = false; const filterLower = filterName.toLowerCase(); if (filterLower || onlyNamed) { if (accountInfo) { - const { accountId, accountIndex, identity, nickname } = accountInfo; - const hasAddressMatch = (!!accountId && accountId.toString().includes(filterName)) || (!!accountIndex && accountIndex.toString().includes(filterName)); + const { accountId, accountIndex, domain, identity, nickname } = accountInfo; + + const hasAddressMatch = ( + (!!accountId && accountId.toString().includes(filterName)) || + (!!accountIndex && accountIndex.toString().includes(filterName)) || + (domain && domain.includes(filterLower)) + ); if (!onlyNamed && hasAddressMatch) { isVisible = true; diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 4b6b85079758..3eb329c95214 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -8,6 +8,7 @@ export { useAccountId } from './useAccountId.js'; export { useAccountInfo } from './useAccountInfo.js'; export { useAccounts } from './useAccounts.js'; export { useAddresses } from './useAddresses.js'; +export { useAddressToDomain, systemNameToChainId } from './useAddressToDomain.js'; export { useAlephBFTCommittee } from './useAlephBFTCommittee.js'; export { useApi } from './useApi.js'; export { useApiStats } from './useApiStats.js'; diff --git a/packages/react-hooks/src/useAddressToDomain.ts b/packages/react-hooks/src/useAddressToDomain.ts new file mode 100644 index 000000000000..eaf167f64de4 --- /dev/null +++ b/packages/react-hooks/src/useAddressToDomain.ts @@ -0,0 +1,32 @@ +// Copyright 2017-2023 @polkadot/app-contracts authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SupportedChainId } from '@azns/resolver-core'; +import { useResolveAddressToDomain } from '@azns/resolver-react'; + +import { useApi } from './useApi.js'; + +export const systemNameToChainId: Map = new Map([ + ['Aleph Zero', SupportedChainId.AlephZero], + ['Aleph Zero Testnet', SupportedChainId.AlephZeroTestnet] +]); + +export const useAddressToDomain = (address: string | undefined): ReturnType => { + const { api, systemChain } = useApi(); + + const chainId = systemNameToChainId.get(systemChain); + + const results = useResolveAddressToDomain(address, { chainId, customApi: api }); + + if (!chainId) { + return { + allPrimaryDomains: undefined, + error: undefined, + hasError: false, + isLoading: false, + primaryDomain: undefined + }; + } + + return results; +}; diff --git a/packages/react-params/src/Param/Static.tsx b/packages/react-params/src/Param/Static.tsx index 2c257a9e5ad7..29b8afc2ad13 100644 --- a/packages/react-params/src/Param/Static.tsx +++ b/packages/react-params/src/Param/Static.tsx @@ -45,7 +45,7 @@ function StaticParam ({ asHex, children, childrenPre, className = '', defaultVal {value || (isOptional ? <>  : t(''))}} + value={
{value?.toString() || (isOptional ? <>  : t(''))}
} /> {children} diff --git a/yarn.lock b/yarn.lock index fd301b0591ce..23654dfe79f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,34 @@ __metadata: languageName: node linkType: hard +"@azns/resolver-core@npm:1.4.0, @azns/resolver-core@npm:^1.4.0": + version: 1.4.0 + resolution: "@azns/resolver-core@npm:1.4.0" + peerDependencies: + "@polkadot/api": ">=10" + "@polkadot/api-contract": ">=10" + "@polkadot/types": ">=10" + "@polkadot/util": ">=11.1.3" + "@polkadot/util-crypto": ">=11.1.3" + bufferutil: ">=4.0.1" + loglevel: ">=1.8" + utf-8-validate: ">=6" + checksum: 45e1a43e5258c48c96c5c34a13e7ff58986f78e3d3d1659a2bae97b40232d3c548507dfd1cddcab4135ccedea563c3e0a421aaa9199a2d85c142ffb56cffad1c + languageName: node + linkType: hard + +"@azns/resolver-react@npm:^1.5.0": + version: 1.5.0 + resolution: "@azns/resolver-react@npm:1.5.0" + dependencies: + "@azns/resolver-core": 1.4.0 + peerDependencies: + react: ">=18" + react-dom: ">=18" + checksum: 8faa0a038b1a525b7d9ac49a6e638456ef61b7c2dff5502d650e42ed85524331939eaf546a19fb2f8862aff7476bbd6e971e97b028f776803cfba473e88f836c + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.18.6": version: 7.18.6 resolution: "@babel/code-frame@npm:7.18.6" @@ -1500,6 +1528,7 @@ __metadata: version: 0.0.0-use.local resolution: "@polkadot/apps-config@workspace:packages/apps-config" dependencies: + "@azns/resolver-core": ^1.4.0 "@polkadot/api": ^10.3.4 "@polkadot/api-derive": ^10.3.4 "@polkadot/networks": ^11.1.3 @@ -3133,14 +3162,14 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*": - version: 17.0.44 - resolution: "@types/react@npm:17.0.44" +"@types/react@npm:^18.2.21": + version: 18.2.21 + resolution: "@types/react@npm:18.2.21" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: ebee02778ca08f954c316dc907802264e0121c87b8fa2e7e0156ab0ef2a1b0a09d968c016a3600ec4c9a17dc09b4274f292d9b15a1a5369bb7e4072def82808f + checksum: ffed203bfe7aad772b8286f7953305c9181ac3a8f27d3f5400fbbc2a8e27ca8e5bbff818ee014f39ca0d19d2b3bb154e5bdbec7e232c6f80b59069375aa78349 languageName: node linkType: hard @@ -4473,6 +4502,8 @@ __metadata: version: 0.0.0-use.local resolution: "azero.dev@workspace:." dependencies: + "@azns/resolver-core": ^1.4.0 + "@azns/resolver-react": ^1.5.0 "@crustio/crust-pin": ^1.0.0 "@pinata/sdk": ^1.2.1 "@polkadot/dev": ^0.72.43 @@ -4492,6 +4523,7 @@ __metadata: electron-builder: 23.6.0 electron-builder-notarize: ^1.5.1 i18next-scanner: ^4.2.0 + loglevel: ^1.8.1 react: ^18.2.0 react-dom: ^18.2.0 react-is: ^18.2.0 @@ -11296,6 +11328,13 @@ fsevents@~2.3.2: languageName: node linkType: hard +"loglevel@npm:^1.8.1": + version: 1.8.1 + resolution: "loglevel@npm:1.8.1" + checksum: a1a62db40291aaeaef2f612334c49e531bff71cc1d01a2acab689ab80d59e092f852ab164a5aedc1a752fdc46b7b162cb097d8a9eb2cf0b299511106c29af61d + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0"