diff --git a/.papi/descriptors/.gitignore b/.papi/descriptors/.gitignore new file mode 100644 index 0000000..46d96ea --- /dev/null +++ b/.papi/descriptors/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!package.json \ No newline at end of file diff --git a/.papi/descriptors/package.json b/.papi/descriptors/package.json new file mode 100644 index 0000000..5b6b71f --- /dev/null +++ b/.papi/descriptors/package.json @@ -0,0 +1,24 @@ +{ + "version": "0.1.0-autogenerated.10165647742034513517", + "name": "@polkadot-api/descriptors", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "browser": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "sideEffects": false, + "peerDependencies": { + "polkadot-api": "*" + } +} diff --git a/.papi/metadata/polkadot_people.scale b/.papi/metadata/polkadot_people.scale new file mode 100644 index 0000000..7f58fb0 Binary files /dev/null and b/.papi/metadata/polkadot_people.scale differ diff --git a/.papi/polkadot-api.json b/.papi/polkadot-api.json new file mode 100644 index 0000000..c39f9fe --- /dev/null +++ b/.papi/polkadot-api.json @@ -0,0 +1,10 @@ +{ + "version": 0, + "descriptorPath": ".papi/descriptors", + "entries": { + "polkadot_people": { + "chain": "polkadot_people", + "metadata": ".papi/metadata/polkadot_people.scale" + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 4e1ddba..bb62f7f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "postinstall": "papi --whitelist whitelist.ts" }, "prettier": { "semi": false @@ -16,6 +17,7 @@ "@headlessui/react": "^2.2.0", "@ledgerhq/hw-transport-webusb": "^6.29.4", "@noble/hashes": "^1.5.0", + "@polkadot-api/descriptors": "file:.papi/descriptors", "@polkadot-api/ledger-signer": "^0.1.6", "@polkadot-api/metadata-builders": "^0.9.1", "@polkadot-api/observable-client": "^0.5.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 322ae01..f242b29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: '@noble/hashes': specifier: ^1.5.0 version: 1.5.0 + '@polkadot-api/descriptors': + specifier: file:.papi/descriptors + version: file:.papi/descriptors(polkadot-api@1.6.5(jiti@1.21.6)(postcss@8.4.47)(rxjs@7.8.1)(smoldot@2.0.30)(yaml@2.6.0)) '@polkadot-api/ledger-signer': specifier: ^0.1.6 version: 0.1.6(@ledgerhq/hw-transport@6.31.4) @@ -728,6 +731,11 @@ packages: '@polkadot-api/codegen@0.12.7': resolution: {integrity: sha512-V1DY+ij+hnsT8IMLteSxNJC3dB0259/T+27gS2mHhqGxIw8qKBQqoxa+mB9TvWuwPkKs3RFyRCgdrlsV1atHPw==} + '@polkadot-api/descriptors@file:.papi/descriptors': + resolution: {directory: .papi/descriptors, type: directory} + peerDependencies: + polkadot-api: '*' + '@polkadot-api/ink-contracts@0.1.2': resolution: {integrity: sha512-RB/BaP6VOfokVWHE5w1NDwP6Qv4tNGDZ9lJawmraL3M3nWG1GlyV+fQxammrQSTNwGvTAVO0it77j88X5uf/7w==} @@ -3278,6 +3286,10 @@ snapshots: '@polkadot-api/substrate-bindings': 0.9.3 '@polkadot-api/utils': 0.1.2 + '@polkadot-api/descriptors@file:.papi/descriptors(polkadot-api@1.6.5(jiti@1.21.6)(postcss@8.4.47)(rxjs@7.8.1)(smoldot@2.0.30)(yaml@2.6.0))': + dependencies: + polkadot-api: 1.6.5(jiti@1.21.6)(postcss@8.4.47)(rxjs@7.8.1)(smoldot@2.0.30)(yaml@2.6.0) + '@polkadot-api/ink-contracts@0.1.2': dependencies: '@polkadot-api/metadata-builders': 0.9.1 diff --git a/src/chain.state.ts b/src/chain.state.ts index 996c6ee..d7a537b 100644 --- a/src/chain.state.ts +++ b/src/chain.state.ts @@ -123,7 +123,7 @@ async function getMetadata(client: PolkadotClient): Promise { } let smoldot: Client | null = null -function getProvider(source: ChainSource) { +export function getProvider(source: ChainSource) { if (source.type === "websocket") { return withPolkadotSdkCompat(getWsProvider(source.value)) } diff --git a/src/codec-components/EditCodec/CAccountId.tsx b/src/codec-components/EditCodec/CAccountId.tsx index 39e5554..c059332 100644 --- a/src/codec-components/EditCodec/CAccountId.tsx +++ b/src/codec-components/EditCodec/CAccountId.tsx @@ -1,11 +1,12 @@ +import { PolkadotIdenticon } from "@/components/PolkadotIdenticon" +import { useSynchronizeInput } from "@/components/useSynchroniseInput" import { accountDetail$, accounts$, getAccountMapKey, getPublicKey, } from "@/extension-accounts.state" -import { PolkadotIdenticon } from "@/components/PolkadotIdenticon" -import { useSynchronizeInput } from "@/components/useSynchroniseInput" +import { identity$, isVerified } from "@/identity.state" import { EditAccountId, NOTIN } from "@codec-components" import { Combobox, @@ -18,17 +19,51 @@ import { getSs58AddressInfo, SS58String, } from "@polkadot-api/substrate-bindings" -import { useStateObservable } from "@react-rxjs/core" +import { state, useStateObservable } from "@react-rxjs/core" +import { combineKeys } from "@react-rxjs/utils" +import { CheckCircle } from "lucide-react" import { FC, useState } from "react" +import { map, switchMap, take } from "rxjs" import { twMerge } from "tailwind-merge" +const hintedAccounts$ = state( + combineKeys( + accounts$.pipe(map((accounts) => [...accounts.keys()])), + (account) => + accounts$.pipe( + map((v) => v.get(account)!), + take(1), + switchMap((details) => + identity$(details.address).pipe( + map((identity) => ({ + address: details.address, + name: identity?.displayName ?? details.name, + isVerified: isVerified(identity), + })), + ), + ), + ), + ).pipe(map((v) => new Map(v))), + new Map< + string, + { + address: string + name: string | undefined + isVerified: boolean | undefined + } + >(), +) + export const CAccountId: EditAccountId = ({ value, onValueChanged }) => { const [localInput, setLocalInput] = useSynchronizeInput( value, onValueChanged, parseValue, ) - const accounts = useStateObservable(accounts$) + const accounts = useStateObservable(hintedAccounts$) + const valueIdentity = useStateObservable( + identity$(value === NOTIN ? "" : value), + ) const [query, setQuery] = useState("") const queryInfo = getSs58AddressInfo(query) @@ -64,10 +99,12 @@ export const CAccountId: EditAccountId = ({ value, onValueChanged }) => { /> )} - accounts.get(getAccountMapKey(address))?.name ?? address + valueIdentity?.displayName ?? + accounts.get(getAccountMapKey(address))?.name ?? + address } onChange={(event) => setQuery(event.target.value)} /> @@ -103,6 +140,9 @@ const AccountOption: FC<{ account: string; publicKey: Uint8Array | null }> = ({ publicKey, }) => { const details = useStateObservable(accountDetail$(account)) + const identity = useStateObservable(identity$(account)) + + const name = identity?.displayName ?? details?.name return ( = ({ size={32} />
- {details?.name && {details.name}} + {name && ( + + {name} + {isVerified(identity) && ( + + )} + + )} {account} diff --git a/src/components/AccountIdDisplay.tsx b/src/components/AccountIdDisplay.tsx index 3507114..248485f 100644 --- a/src/components/AccountIdDisplay.tsx +++ b/src/components/AccountIdDisplay.tsx @@ -1,6 +1,8 @@ -import { accountDetail$, getPublicKey } from "@/extension-accounts.state" import { PolkadotIdenticon } from "@/components/PolkadotIdenticon" +import { accountDetail$, getPublicKey } from "@/extension-accounts.state" +import { identity$, isVerified } from "@/identity.state" import { useStateObservable } from "@react-rxjs/core" +import { CheckCircle } from "lucide-react" import { SS58String } from "polkadot-api" import { FC } from "react" import { twMerge } from "tailwind-merge" @@ -10,6 +12,9 @@ export const AccountIdDisplay: FC<{ className?: string }> = ({ value, className }) => { const details = useStateObservable(accountDetail$(value)) + const identity = useStateObservable(identity$(value)) + + const name = identity?.displayName ?? details?.name return (
@@ -19,7 +24,14 @@ export const AccountIdDisplay: FC<{ size={32} />
- {details?.name && {details.name}} + {name && ( + + {name} + {isVerified(identity) && ( + + )} + + )} {value} diff --git a/src/components/Select.tsx b/src/components/Select.tsx index f5ce18c..0ce8536 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -37,7 +37,7 @@ export const SearchableSelect = ({ >
options.find((_option) => _option.value === option)?.text ?? "" } diff --git a/src/identity.state.ts b/src/identity.state.ts new file mode 100644 index 0000000..97f3141 --- /dev/null +++ b/src/identity.state.ts @@ -0,0 +1,77 @@ +import { + IdentityData, + IdentityJudgement, + polkadot_people, +} from "@polkadot-api/descriptors" +import { state } from "@react-rxjs/core" +import { Binary, createClient, SS58String } from "polkadot-api" +import { chainSpec as relayChain } from "polkadot-api/chains/polkadot" +import { chainSpec } from "polkadot-api/chains/polkadot_people" +import { catchError, map, of, tap } from "rxjs" +import { getProvider } from "./chain.state" +import { localStorageSubject } from "./utils/localStorageSubject" + +export interface Identity { + displayName: string + judgments: Array<{ + registrar: number + judgement: IdentityJudgement["type"] + }> +} +export const isVerified = (identity: Identity | null) => + identity?.judgments.some((j) => j.judgement === "Reasonable") + +const cache = localStorageSubject>( + "identity-cache", + JSON, + {}, +) + +const client = createClient( + getProvider({ + id: "polkadot_people", + type: "chainSpec", + value: { + chainSpec, + relayChain, + }, + }), +) +const typedApi = client.getTypedApi(polkadot_people) + +export const identity$ = state( + (address: SS58String) => + typedApi.query.Identity.IdentityOf.watchValue(address).pipe( + map((res): Identity | null => { + const displayName = res && readIdentityData(res[0].info.display) + return displayName + ? { + displayName: displayName.asText(), + judgments: res[0].judgements.map(([registrar, judgement]) => ({ + registrar, + judgement: judgement.type, + })), + } + : null + }), + tap((v) => + cache.setValue((c) => { + if (v) { + return { ...c, [address]: v } + } else { + delete c[address] + return c + } + }), + ), + catchError(() => of(null)), + ), + (address) => cache.getValue()[address] ?? null, +) + +const readIdentityData = (identityData: IdentityData): Binary | null => { + if (identityData.type === "None" || identityData.type === "Raw0") return null + if (identityData.type === "Raw1") + return Binary.fromBytes(new Uint8Array(identityData.value)) + return identityData.value +} diff --git a/src/utils/localStorageSubject.ts b/src/utils/localStorageSubject.ts new file mode 100644 index 0000000..1661860 --- /dev/null +++ b/src/utils/localStorageSubject.ts @@ -0,0 +1,55 @@ +import { BehaviorSubject, Observable } from "rxjs" + +interface Formatter { + parse: (value: string) => R + stringify: (value: string) => string +} +interface LocalStorageSubject { + ( + key: string, + formatter: Formatter, + ): { + stream$: Observable + setValue: (v: R | ((old: R) => R)) => void + getValue: () => R | null + clear: () => void + } + ( + key: string, + formatter: Formatter, + defaultValue: R, + ): { + stream$: Observable + setValue: (v: R | ((old: R) => R)) => void + getValue: () => R + clear: () => void + } +} +export const localStorageSubject: LocalStorageSubject = ( + key, + formatter, + defaultValue = null, +) => { + const subject = new BehaviorSubject(defaultValue) + try { + const value = localStorage.getItem(key) + subject.next(value ? formatter.parse(value) : defaultValue) + } catch (_) { + /* empty */ + } + + return { + stream$: subject.asObservable(), + setValue: (value) => { + const reducer = typeof value === "function" ? value : () => value + const newValue = (reducer as any)(subject.getValue()) + localStorage.setItem(key, formatter.stringify(newValue)) + subject.next(newValue) + }, + getValue: () => subject.getValue(), + clear: () => { + localStorage.removeItem(key) + subject.next(defaultValue) + }, + } +} diff --git a/whitelist.ts b/whitelist.ts new file mode 100644 index 0000000..8188588 --- /dev/null +++ b/whitelist.ts @@ -0,0 +1,5 @@ +import { Polkadot_peopleWhitelistEntry } from "@polkadot-api/descriptors" + +export const whitelist: Polkadot_peopleWhitelistEntry[] = [ + "query.Identity.IdentityOf", +]