feat: improve AccountId input
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"@headlessui/react": "^2.1.10",
|
||||
"@polkadot-api/known-chains": "^0.5.4",
|
||||
"@polkadot-api/metadata-builders": "^0.8.2",
|
||||
"@polkadot-api/pjs-signer": "^0.5.0",
|
||||
"@polkadot-api/polkadot-sdk-compat": "^2.3.1",
|
||||
"@polkadot-api/sm-provider": "^0.1.3",
|
||||
"@polkadot-api/smoldot": "^0.3.3",
|
||||
|
||||
Generated
+16
@@ -14,6 +14,9 @@ dependencies:
|
||||
'@polkadot-api/metadata-builders':
|
||||
specifier: ^0.8.2
|
||||
version: 0.8.2
|
||||
'@polkadot-api/pjs-signer':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
'@polkadot-api/polkadot-sdk-compat':
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
@@ -783,12 +786,25 @@ packages:
|
||||
'@polkadot-api/utils': 0.1.2
|
||||
dev: false
|
||||
|
||||
/@polkadot-api/pjs-signer@0.5.0:
|
||||
resolution: {integrity: sha512-rvQIEG5lbtZ2/ix7QFYTy+2ZxZ3Gi9pzhXEUFpMTmHvYqAGj0RrK94v544bzdV8XBAotgGV6jqIdIArd0p+Tog==}
|
||||
dependencies:
|
||||
'@polkadot-api/metadata-builders': 0.8.2
|
||||
'@polkadot-api/polkadot-signer': 0.1.6
|
||||
'@polkadot-api/substrate-bindings': 0.9.2
|
||||
'@polkadot-api/utils': 0.1.2
|
||||
dev: false
|
||||
|
||||
/@polkadot-api/polkadot-sdk-compat@2.3.1:
|
||||
resolution: {integrity: sha512-rb8IWmPRhKWD9NG4zh2n4q0HlEAvq+Cv1CbD+8YxH0XAqIIiFA+ch5JeDCIxQYngkn/43B0Gs7Gtzh18yv2yoA==}
|
||||
dependencies:
|
||||
'@polkadot-api/json-rpc-provider': 0.0.4
|
||||
dev: false
|
||||
|
||||
/@polkadot-api/polkadot-signer@0.1.6:
|
||||
resolution: {integrity: sha512-X7ghAa4r7doETtjAPTb50IpfGtrBmy3BJM5WCfNKa1saK04VFY9w+vDn+hwEcM4p0PcDHt66Ts74hzvHq54d9A==}
|
||||
dev: false
|
||||
|
||||
/@polkadot-api/sm-provider@0.1.3(smoldot@2.0.30):
|
||||
resolution: {integrity: sha512-/Sj6fPlUtu7BVEOeIxjflrO28G564fxM3JDjQZcv1V3hrAb22/p6I/toWej0zSZ6mEAXLjCIEWje14b/lyZmkw==}
|
||||
peerDependencies:
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// Based from https://github.com/paritytech/oo7/blob/251ba2b7c45503b68eab4320c270b5afa9bccb60/packages/polkadot-identicon/src/index.jsx
|
||||
|
||||
import { Blake2256 } from "@polkadot-api/substrate-bindings"
|
||||
import { FC, SVGAttributes } from "react"
|
||||
|
||||
export const PolkadotIdenticon: FC<
|
||||
Omit<SVGAttributes<SVGElement>, "viewBox"> & {
|
||||
sixPoint?: boolean
|
||||
publicKey: Uint8Array | null
|
||||
size?: number
|
||||
}
|
||||
> = ({ sixPoint, publicKey, ...props }) => {
|
||||
const svgProps = {
|
||||
...props,
|
||||
width: props.size ?? props.width ?? SIZE,
|
||||
height: props.size ?? props.height ?? SIZE,
|
||||
viewBox: `0 0 ${SIZE} ${SIZE}`,
|
||||
}
|
||||
if (!publicKey) {
|
||||
return (
|
||||
<svg {...svgProps}>
|
||||
<circle cx={SIZE / 2} cy={SIZE / 2} r={SIZE / 2} fill="#eee" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const colors = getColors(publicKey)
|
||||
const circles = getPositions(sixPoint)
|
||||
|
||||
return (
|
||||
<svg {...svgProps}>
|
||||
<circle cx={SIZE / 2} cy={SIZE / 2} r={SIZE / 2} fill="#eee" />
|
||||
{circles.map(({ x, y }, i) => (
|
||||
<circle key={i} cx={x} cy={y} r={z} fill={colors[i]} />
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const SIZE = 64
|
||||
const CENTER = SIZE / 2
|
||||
|
||||
const z = (SIZE / 64) * 5
|
||||
type Schema = { freq: number; colors: number[] }
|
||||
const schema: Record<string, Schema> = {
|
||||
target: {
|
||||
freq: 1,
|
||||
colors: [0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 1],
|
||||
},
|
||||
cube: {
|
||||
freq: 20,
|
||||
colors: [0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 5],
|
||||
},
|
||||
quazar: {
|
||||
freq: 16,
|
||||
colors: [1, 2, 3, 1, 2, 4, 5, 5, 4, 1, 2, 3, 1, 2, 4, 5, 5, 4, 0],
|
||||
},
|
||||
flower: {
|
||||
freq: 32,
|
||||
colors: [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 3],
|
||||
},
|
||||
cyclic: {
|
||||
freq: 32,
|
||||
colors: [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
vmirror: {
|
||||
freq: 128,
|
||||
colors: [0, 1, 2, 3, 4, 5, 3, 4, 2, 0, 1, 6, 7, 8, 9, 7, 8, 6, 10],
|
||||
},
|
||||
hmirror: {
|
||||
freq: 128,
|
||||
colors: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 8, 6, 7, 5, 3, 4, 2, 11],
|
||||
},
|
||||
}
|
||||
const totalFreq = Object.keys(schema)
|
||||
.map((k) => schema[k].freq)
|
||||
.reduce((a, b) => a + b)
|
||||
|
||||
const findScheme = (d: number) => {
|
||||
let cum = 0
|
||||
for (const scheme of Object.values(schema)) {
|
||||
cum += scheme.freq
|
||||
if (d < cum) {
|
||||
return scheme
|
||||
}
|
||||
}
|
||||
throw "Unreachable"
|
||||
}
|
||||
const zero = Blake2256(
|
||||
new Uint8Array([
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0,
|
||||
]),
|
||||
)
|
||||
|
||||
const getColors = (publicKey: Uint8Array) => {
|
||||
const id = Array.from(Blake2256(publicKey)).map(
|
||||
(x, i) => (x + 256 - zero[i]) % 256,
|
||||
)
|
||||
|
||||
const sat = (Math.floor((id[29] * 70) / 256 + 26) % 80) + 30
|
||||
const d = Math.floor((id[30] + id[31] * 256) % totalFreq)
|
||||
const scheme = findScheme(d)
|
||||
const palette = Array.from(id).map((x, i) => {
|
||||
const b = (x + (i % 28) * 58) % 256
|
||||
if (b == 0) {
|
||||
return "#444"
|
||||
}
|
||||
if (b == 255) {
|
||||
return "transparent"
|
||||
}
|
||||
const h = Math.floor(((b % 64) * 360) / 64)
|
||||
const l = [53, 15, 35, 75][Math.floor(b / 64)]
|
||||
return `hsl(${h}, ${sat}%, ${l}%)`
|
||||
})
|
||||
|
||||
const rot = (id[28] % 6) * 3
|
||||
|
||||
return scheme.colors.map(
|
||||
(_, i) => palette[scheme.colors[i < 18 ? (i + rot) % 18 : 18]],
|
||||
)
|
||||
}
|
||||
|
||||
const getPositions = (sixPoint = false) => {
|
||||
const r = sixPoint ? (SIZE / 2 / 8) * 5 : (SIZE / 2 / 4) * 3
|
||||
const rroot3o2 = (r * Math.sqrt(3)) / 2
|
||||
const ro2 = r / 2
|
||||
const rroot3o4 = (r * Math.sqrt(3)) / 4
|
||||
const ro4 = r / 4
|
||||
const r3o4 = (r * 3) / 4
|
||||
|
||||
return [
|
||||
{ x: CENTER, y: CENTER - r },
|
||||
{ x: CENTER, y: CENTER - ro2 },
|
||||
{ x: CENTER - rroot3o4, y: CENTER - r3o4 },
|
||||
{ x: CENTER - rroot3o2, y: CENTER - ro2 },
|
||||
{ x: CENTER - rroot3o4, y: CENTER - ro4 },
|
||||
{ x: CENTER - rroot3o2, y: CENTER },
|
||||
{ x: CENTER - rroot3o2, y: CENTER + ro2 },
|
||||
{ x: CENTER - rroot3o4, y: CENTER + ro4 },
|
||||
{ x: CENTER - rroot3o4, y: CENTER + r3o4 },
|
||||
{ x: CENTER, y: CENTER + r },
|
||||
{ x: CENTER, y: CENTER + ro2 },
|
||||
{ x: CENTER + rroot3o4, y: CENTER + r3o4 },
|
||||
{ x: CENTER + rroot3o2, y: CENTER + ro2 },
|
||||
{ x: CENTER + rroot3o4, y: CENTER + ro4 },
|
||||
{ x: CENTER + rroot3o2, y: CENTER },
|
||||
{ x: CENTER + rroot3o2, y: CENTER - ro2 },
|
||||
{ x: CENTER + rroot3o4, y: CENTER - ro4 },
|
||||
{ x: CENTER + rroot3o4, y: CENTER - r3o4 },
|
||||
{ x: CENTER, y: CENTER },
|
||||
]
|
||||
}
|
||||
@@ -1,10 +1,40 @@
|
||||
import {
|
||||
SS58String,
|
||||
getSs58AddressInfo,
|
||||
} from "@polkadot-api/substrate-bindings"
|
||||
import { EditAccountId, NOTIN } from "@codec-components"
|
||||
import { TextInputField } from "./codec-components"
|
||||
import { PolkadotIdenticon } from "@/components/PolkadotIdenticon"
|
||||
import { useSynchronizeInput } from "@/components/useSynchroniseInput"
|
||||
import { EditAccountId, NOTIN } from "@codec-components"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/react"
|
||||
import {
|
||||
connectInjectedExtension,
|
||||
getInjectedExtensions,
|
||||
InjectedPolkadotAccount,
|
||||
} from "@polkadot-api/pjs-signer"
|
||||
import {
|
||||
getSs58AddressInfo,
|
||||
SS58String,
|
||||
} from "@polkadot-api/substrate-bindings"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { combineKeys } from "@react-rxjs/utils"
|
||||
import { FC, useState } from "react"
|
||||
import {
|
||||
concat,
|
||||
filter,
|
||||
finalize,
|
||||
from,
|
||||
fromEventPattern,
|
||||
interval,
|
||||
map,
|
||||
retry,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
timer,
|
||||
} from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export const CAccountId: EditAccountId = ({ value, onValueChanged }) => {
|
||||
const [localInput, setLocalInput] = useSynchronizeInput(
|
||||
@@ -12,30 +42,167 @@ export const CAccountId: EditAccountId = ({ value, onValueChanged }) => {
|
||||
onValueChanged,
|
||||
parseValue,
|
||||
)
|
||||
const accounts = useStateObservable(accounts$)
|
||||
|
||||
const [query, setQuery] = useState("")
|
||||
const queryInfo = getSs58AddressInfo(query)
|
||||
const filteredAccounts =
|
||||
query === ""
|
||||
? Array.from(accounts.values())
|
||||
: Array.from(accounts.values()).filter(
|
||||
(account) =>
|
||||
account.address.toLowerCase().includes(query.toLowerCase()) ||
|
||||
(account.name &&
|
||||
account.name.toLowerCase().includes(query.toLowerCase())),
|
||||
)
|
||||
|
||||
const invalid = getValidationError(localInput)
|
||||
return (
|
||||
<TextInputField
|
||||
value={localInput ?? ""}
|
||||
className="min-w-96"
|
||||
onChange={setLocalInput}
|
||||
placeholder="AccountId"
|
||||
warn={localInput === ""}
|
||||
softError={invalid}
|
||||
/>
|
||||
<Combobox
|
||||
value={localInput}
|
||||
onChange={(v) => setLocalInput(v ?? "")}
|
||||
onClose={() => setQuery("")}
|
||||
immediate
|
||||
>
|
||||
<ComboboxButton
|
||||
className={twMerge(
|
||||
"text-sm border rounded p-2 border-polkadot-200 leading-tight text-white focus-within:outline",
|
||||
"flex items-center gap-2 bg-polkadot-900",
|
||||
value === NOTIN ? "border-orange-400" : null,
|
||||
)}
|
||||
>
|
||||
{localInput && (
|
||||
<PolkadotIdenticon
|
||||
className="flex-shrink-0"
|
||||
publicKey={getPublicKey(localInput)}
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
<ComboboxInput
|
||||
className="bg-transparent outline-none w-full"
|
||||
aria-label="AccountId"
|
||||
displayValue={(address: string) =>
|
||||
accounts.get(address)?.name ?? address
|
||||
}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
</ComboboxButton>
|
||||
<ComboboxOptions
|
||||
anchor="bottom start"
|
||||
className={twMerge(
|
||||
"max-h-96 rounded border border-black p-2 bg-polkadot-800 [--anchor-gap:4px]",
|
||||
)}
|
||||
>
|
||||
{filteredAccounts.length > 0 ? (
|
||||
filteredAccounts.map((account) => (
|
||||
<AccountOption
|
||||
key={account.address}
|
||||
account={account.address}
|
||||
publicKey={getPublicKey(account.address)}
|
||||
/>
|
||||
))
|
||||
) : queryInfo.isValid ? (
|
||||
<AccountOption account={query} publicKey={queryInfo.publicKey} />
|
||||
) : (
|
||||
<div className="text-slate-400">
|
||||
The value is not a valid Account ID
|
||||
</div>
|
||||
)}
|
||||
</ComboboxOptions>
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
||||
const AccountOption: FC<{ account: string; publicKey: Uint8Array | null }> = ({
|
||||
account,
|
||||
publicKey,
|
||||
}) => {
|
||||
const details = useStateObservable(accountDetail$(account))
|
||||
|
||||
return (
|
||||
<ComboboxOption
|
||||
value={account}
|
||||
className="data-[focus]:bg-polkadot-700 cursor-pointer flex flex-row items-center gap-2 p-1 rounded overflow-hidden"
|
||||
>
|
||||
<PolkadotIdenticon
|
||||
className="flex-shrink-0"
|
||||
publicKey={publicKey}
|
||||
size={32}
|
||||
/>
|
||||
<div className="flex flex-col justify-center text-white leading-tight overflow-hidden">
|
||||
{details?.name && <span>{details.name}</span>}
|
||||
<span className="text-slate-400 text-ellipsis overflow-hidden">
|
||||
{account}
|
||||
</span>
|
||||
</div>
|
||||
</ComboboxOption>
|
||||
)
|
||||
}
|
||||
|
||||
const getPublicKey = (address: string) => {
|
||||
const info = getSs58AddressInfo(address)
|
||||
return info.isValid ? info.publicKey : null
|
||||
}
|
||||
|
||||
const getValidationError = (value: string) => {
|
||||
if (value === "") return null
|
||||
const info = getSs58AddressInfo(value)
|
||||
return info.isValid &&
|
||||
(info.publicKey.length === 32 || info.publicKey.length === 32)
|
||||
? null
|
||||
: "Invalid address"
|
||||
const { isValid } = getSs58AddressInfo(value)
|
||||
return isValid ? null : "Invalid address"
|
||||
}
|
||||
const parseValue = (value: string): SS58String | NOTIN => {
|
||||
if (value.trim() === "") return NOTIN
|
||||
if (getValidationError(value)) return NOTIN
|
||||
return value
|
||||
}
|
||||
|
||||
const extensions$ = state(
|
||||
concat(
|
||||
timer(0, 100).pipe(
|
||||
map(getInjectedExtensions),
|
||||
filter((v) => v.length > 0),
|
||||
take(1),
|
||||
),
|
||||
interval(2000).pipe(map(getInjectedExtensions)),
|
||||
),
|
||||
[],
|
||||
)
|
||||
const extensionAccounts$ = combineKeys(extensions$, (extension) =>
|
||||
from(connectInjectedExtension(extension)).pipe(
|
||||
switchMap((extension) =>
|
||||
fromEventPattern<InjectedPolkadotAccount[]>(
|
||||
extension.subscribe,
|
||||
(_, fn) => fn(),
|
||||
).pipe(
|
||||
startWith(extension.getAccounts()),
|
||||
finalize(extension.disconnect),
|
||||
),
|
||||
),
|
||||
retry({
|
||||
delay: 100,
|
||||
}),
|
||||
),
|
||||
)
|
||||
const accounts$ = state(
|
||||
extensionAccounts$.pipe(
|
||||
map(
|
||||
(extensionAccounts) =>
|
||||
new Map(
|
||||
Array.from(extensionAccounts.entries()).flatMap(
|
||||
([extension, accounts]) =>
|
||||
accounts.map((account) => [
|
||||
account.address,
|
||||
{
|
||||
...account,
|
||||
extension,
|
||||
},
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
new Map<string, InjectedPolkadotAccount & { extension: string }>(),
|
||||
)
|
||||
|
||||
const accountDetail$ = state(
|
||||
(account: string) => accounts$.pipe(map((v) => v.get(account) ?? null)),
|
||||
null,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user