Merge branch 'vo-identity'
This commit is contained in:
3
.papi/descriptors/.gitignore
vendored
Normal file
3
.papi/descriptors/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!package.json
|
||||
24
.papi/descriptors/package.json
Normal file
24
.papi/descriptors/package.json
Normal file
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
BIN
.papi/metadata/polkadot_people.scale
Normal file
BIN
.papi/metadata/polkadot_people.scale
Normal file
Binary file not shown.
10
.papi/polkadot-api.json
Normal file
10
.papi/polkadot-api.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 0,
|
||||
"descriptorPath": ".papi/descriptors",
|
||||
"entries": {
|
||||
"polkadot_people": {
|
||||
"chain": "polkadot_people",
|
||||
"metadata": ".papi/metadata/polkadot_people.scale"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -123,7 +123,7 @@ async function getMetadata(client: PolkadotClient): Promise<AnyMetadata> {
|
||||
}
|
||||
|
||||
let smoldot: Client | null = null
|
||||
function getProvider(source: ChainSource) {
|
||||
export function getProvider(source: ChainSource) {
|
||||
if (source.type === "websocket") {
|
||||
return withPolkadotSdkCompat(getWsProvider(source.value))
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
/>
|
||||
)}
|
||||
<ComboboxInput
|
||||
className="bg-transparent outline-none w-full"
|
||||
className="bg-transparent outline-none w-full select-all"
|
||||
aria-label="AccountId"
|
||||
displayValue={(address: string) =>
|
||||
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 (
|
||||
<ComboboxOption
|
||||
@@ -115,7 +155,14 @@ const AccountOption: FC<{ account: string; publicKey: Uint8Array | null }> = ({
|
||||
size={32}
|
||||
/>
|
||||
<div className="flex flex-col justify-center text-white leading-tight overflow-hidden">
|
||||
{details?.name && <span>{details.name}</span>}
|
||||
{name && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{name}
|
||||
{isVerified(identity) && (
|
||||
<CheckCircle size={16} className="text-green-400" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-slate-400 text-ellipsis overflow-hidden">
|
||||
{account}
|
||||
</span>
|
||||
|
||||
@@ -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 (
|
||||
<div className={twMerge("flex items-center gap-2", className)}>
|
||||
@@ -19,7 +24,14 @@ export const AccountIdDisplay: FC<{
|
||||
size={32}
|
||||
/>
|
||||
<div className="flex flex-col justify-center text-white leading-tight overflow-hidden">
|
||||
{details?.name && <span>{details.name}</span>}
|
||||
{name && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{name}
|
||||
{isVerified(identity) && (
|
||||
<CheckCircle size={16} className="text-green-400" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-slate-300 text-ellipsis overflow-hidden">
|
||||
{value}
|
||||
</span>
|
||||
|
||||
@@ -37,7 +37,7 @@ export const SearchableSelect = <T,>({
|
||||
>
|
||||
<div className="flex flex-row px-4 py-2 border border-polkadot-200 text-white bg-black">
|
||||
<ComboboxInput
|
||||
className="text-ellipsis overflow-hidden whitespace-nowrap focus:outline-none"
|
||||
className="text-ellipsis overflow-hidden whitespace-nowrap focus:outline-none select-all"
|
||||
displayValue={(option: T) =>
|
||||
options.find((_option) => _option.value === option)?.text ?? ""
|
||||
}
|
||||
|
||||
77
src/identity.state.ts
Normal file
77
src/identity.state.ts
Normal file
@@ -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<Record<string, Identity>>(
|
||||
"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
|
||||
}
|
||||
55
src/utils/localStorageSubject.ts
Normal file
55
src/utils/localStorageSubject.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { BehaviorSubject, Observable } from "rxjs"
|
||||
|
||||
interface Formatter<R> {
|
||||
parse: (value: string) => R
|
||||
stringify: (value: string) => string
|
||||
}
|
||||
interface LocalStorageSubject {
|
||||
<R>(
|
||||
key: string,
|
||||
formatter: Formatter<R>,
|
||||
): {
|
||||
stream$: Observable<R | null>
|
||||
setValue: (v: R | ((old: R) => R)) => void
|
||||
getValue: () => R | null
|
||||
clear: () => void
|
||||
}
|
||||
<R>(
|
||||
key: string,
|
||||
formatter: Formatter<R>,
|
||||
defaultValue: R,
|
||||
): {
|
||||
stream$: Observable<R>
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
5
whitelist.ts
Normal file
5
whitelist.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Polkadot_peopleWhitelistEntry } from "@polkadot-api/descriptors"
|
||||
|
||||
export const whitelist: Polkadot_peopleWhitelistEntry[] = [
|
||||
"query.Identity.IdentityOf",
|
||||
]
|
||||
Reference in New Issue
Block a user