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 cdf6d03..6e8284a 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 index c43be5e..97f3141 100644 --- a/src/identity.state.ts +++ b/src/identity.state.ts @@ -3,21 +3,23 @@ import { 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 { localStorageSubject } from "./utils/localStorageSubject" -import { Binary, createClient, SS58String } from "polkadot-api" +import { catchError, map, of, tap } from "rxjs" import { getProvider } from "./chain.state" -import { state } from "@react-rxjs/core" -import { filter, map, merge, take, tap } from "rxjs" +import { localStorageSubject } from "./utils/localStorageSubject" -interface Identity { +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", @@ -37,41 +39,38 @@ const client = createClient( ) const typedApi = client.getTypedApi(polkadot_people) -export const identity$ = state((address: SS58String) => { - const cached$ = cache.stream$.pipe( - take(1), - map((v) => v[address]), - filter((v) => !!v), - ) - const chain$ = typedApi.query.Identity.IdentityOf.watchValue(address).pipe( - map((res): Identity | null => - res - ? { - displayName: readIdentityData(res[0].info.display).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 - } +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)), ), - ) - return merge(cached$, chain$) -}, null) + (address) => cache.getValue()[address] ?? null, +) -const readIdentityData = (identityData: IdentityData): Binary => { - if (identityData.type === "None" || identityData.type === "Raw0") - return Binary.fromHex("") +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 index c71adc1..1661860 100644 --- a/src/utils/localStorageSubject.ts +++ b/src/utils/localStorageSubject.ts @@ -11,6 +11,7 @@ interface LocalStorageSubject { ): { stream$: Observable setValue: (v: R | ((old: R) => R)) => void + getValue: () => R | null clear: () => void } ( @@ -20,6 +21,7 @@ interface LocalStorageSubject { ): { stream$: Observable setValue: (v: R | ((old: R) => R)) => void + getValue: () => R clear: () => void } } @@ -44,6 +46,7 @@ export const localStorageSubject: LocalStorageSubject = ( localStorage.setItem(key, formatter.stringify(newValue)) subject.next(newValue) }, + getValue: () => subject.getValue(), clear: () => { localStorage.removeItem(key) subject.next(defaultValue)