Merge branch 'vo-identity'

This commit is contained in:
Victor Oliva
2024-11-05 15:27:42 +01:00
13 changed files with 259 additions and 12 deletions

3
.papi/descriptors/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!package.json

View 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": "*"
}
}

Binary file not shown.

10
.papi/polkadot-api.json Normal file
View File

@@ -0,0 +1,10 @@
{
"version": 0,
"descriptorPath": ".papi/descriptors",
"entries": {
"polkadot_people": {
"chain": "polkadot_people",
"metadata": ".papi/metadata/polkadot_people.scale"
}
}
}

View File

@@ -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
View File

@@ -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

View File

@@ -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))
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View 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
}

View 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
View File

@@ -0,0 +1,5 @@
import { Polkadot_peopleWhitelistEntry } from "@polkadot-api/descriptors"
export const whitelist: Polkadot_peopleWhitelistEntry[] = [
"query.Identity.IdentityOf",
]