replace account input with AddressInput, fix polkadot vault

This commit is contained in:
Victor Oliva
2025-11-06 09:11:17 +01:00
parent 7adfb9e3aa
commit 7f9b4ad9ae
10 changed files with 188 additions and 882 deletions
+4 -4
View File
@@ -15,17 +15,17 @@
},
"dependencies": {
"@acala-network/chopsticks-core": "^1.2.4",
"@ledgerhq/hw-transport-webhid": "^6.30.8",
"@ledgerhq/hw-transport-webhid": "^6.30.9",
"@polkadot-api/descriptors": "file:.papi/descriptors",
"@polkadot-api/json-rpc-provider-proxy": "^0.2.5",
"@polkadot-api/metadata-builders": "^0.13.7",
"@polkadot-api/observable-client": "^0.16.0",
"@polkadot-api/react-builder": "0.3.9",
"@polkadot-api/react-components": "^0.4.0",
"@polkadot-api/react-components": "^0.4.1",
"@polkadot-api/substrate-bindings": "^0.16.5",
"@polkadot-api/tx-utils": "^0.1.8",
"@polkadot-api/utils": "^0.2.0",
"@polkahub/ui-components": "0.1.0-pre.11",
"@polkahub/ui-components": "0.1.0-pre.15",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
@@ -46,7 +46,7 @@
"idb-keyval": "^6.2.2",
"lucide-react": "^0.552.0",
"polkadot-api": "^1.20.1",
"polkahub": "0.1.0-pre.13",
"polkahub": "0.1.0-pre.15",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-portal": "^4.3.0",
+110 -656
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -9,6 +9,7 @@ import { RpcCalls } from "./pages/RpcCalls"
import { RuntimeCalls } from "./pages/RuntimeCalls"
import { Storage } from "./pages/Storage"
import { Transactions } from "./pages/Transactions"
import { VaultTxModal } from "polkahub"
export default function App() {
return (
@@ -30,6 +31,7 @@ export default function App() {
</div>
</div>
<Transactions />
<VaultTxModal />
</div>
)
}
@@ -1,9 +1,12 @@
import { AccountIdInput } from "@/components/AccountIdInput"
import { EditAccountId, NOTIN } from "@polkadot-api/react-builder"
import { AddressInput } from "polkahub"
export const CAccountId: EditAccountId = ({ value, onValueChanged }) => (
<AccountIdInput
<AddressInput
className="w-64"
triggerClassName="h-9 bg-input"
value={value === NOTIN ? null : value}
onValueChanged={(v) => onValueChanged(v === null ? NOTIN : v)}
onChange={(v) => onValueChanged(v === null ? NOTIN : v)}
disableClear
/>
)
-206
View File
@@ -1,206 +0,0 @@
import { AccountIdDisplay } from "@/components/AccountIdDisplay"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { identity$, isVerified } from "@/state/identity.state"
import { addrToAccount$, getPublicKey } from "@/state/polkahub"
import { cn } from "@/utils/cn"
import {
getSs58AddressInfo,
SS58String,
} from "@polkadot-api/substrate-bindings"
import { toHex } from "@polkadot-api/utils"
import { state, useStateObservable } from "@react-rxjs/core"
import { combineKeys } from "@react-rxjs/utils"
import { Check, ChevronsUpDown } from "lucide-react"
import { FC, useState } from "react"
import { filter, map, switchMap, take } from "rxjs"
const acconutMap$ = addrToAccount$.pipeState(
map((groups) =>
Object.fromEntries(
Object.entries(groups)
.filter(([accountId]) => !accountId.startsWith("0x"))
.map(([accountId, account]) => [
toHex(getPublicKey(accountId)),
account,
]),
),
),
)
const hintedAccounts$ = state(
combineKeys(
acconutMap$.pipe(map((groups) => Object.keys(groups))),
(address) =>
acconutMap$.pipe(
map((v) => v[address]),
filter((v) => !!v),
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 AccountIdInput: FC<{
value: SS58String | null
onValueChanged: (value: SS58String | null) => void
className?: string
}> = ({ value, onValueChanged, className }) => {
const accounts = useStateObservable(hintedAccounts$)
const [query, setQuery] = useState("")
const queryInfo = getSs58AddressInfo(query)
const [open, _setOpen] = useState(false)
const setOpen = (value: boolean) => {
_setOpen(value)
setQuery("")
}
const valueIsNew =
value !== null &&
!accounts.has(toHex(getPublicKey(value) ?? new Uint8Array()))
const accountList = Array.from(accounts.entries())
if (value !== null) {
accountList.sort(([, a], [, b]) =>
a.address === value ? -1 : b.address === value ? 1 : 0,
)
}
const onTriggerKeyDown = (evt: React.KeyboardEvent) => {
if (evt.key.length === 1) {
setOpen(true)
}
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild onKeyDown={onTriggerKeyDown}>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
"flex w-64 justify-between overflow-hidden px-2 border border-border bg-input",
className,
)}
>
{value !== null ? (
<AccountIdDisplay value={value} className="overflow-hidden" />
) : (
<span className="opacity-80">Select…</span>
)}
<ChevronsUpDown size={14} className="opacity-50 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-64 p-0">
<Command>
<CommandInput
placeholder="Filter or insert…"
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>
<div className="text-foreground/50">
The value is not a valid Account ID
</div>
</CommandEmpty>
<CommandGroup>
{valueIsNew && (
<AccountOption
account={value}
selected={true}
onSelect={() => setOpen(false)}
/>
)}
{accountList.map(([key, account]) => (
<AccountOption
key={key}
account={account.address}
selected={value === account.address}
onSelect={() => {
onValueChanged(account.address)
setOpen(false)
}}
/>
))}
{queryInfo.isValid && (
<AccountOption
account={query}
selected={value === query}
onSelect={() => {
onValueChanged(query)
setOpen(false)
}}
/>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
const accountDetail$ = state(
(accountId: SS58String) => acconutMap$.pipe(map((v) => v[accountId] ?? null)),
null,
)
const AccountOption: FC<{
account: string
selected: boolean
onSelect: () => void
}> = ({ account, selected, onSelect }) => {
const details = useStateObservable(accountDetail$(account))
const identity = useStateObservable(identity$(account))
const name = identity?.displayName ?? details?.name
return (
<CommandItem
value={account + "_" + name}
onSelect={onSelect}
className="flex flex-row items-center gap-2 p-1"
>
<AccountIdDisplay value={account} className="overflow-hidden" />
<Check
size={12}
className={cn(
"ml-auto shrink-0",
selected ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
)
}
+1
View File
@@ -78,6 +78,7 @@ export function CommandPopover({
onClick={() => setOpen(true)}
placeholder={placeholder}
className="border-none focus:ring-0"
autoFocus
/>
{open && (
+16 -10
View File
@@ -1,7 +1,9 @@
import { ActionButton } from "@/components/ActionButton"
import {
Dialog,
DialogBody,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
@@ -30,17 +32,21 @@ export const ExtrinsicModal: React.FC<{
)
evt.preventDefault()
}}
className="flex flex-col overflow-hidden"
className="flex flex-col overflow-hidden max-w-lg sm:max-w-lg md:max-w-lg"
>
<DialogTitle>Create TX</DialogTitle>
<Suspense fallback="Loading…">
<SendTransactionForm
callData={
callData instanceof Uint8Array ? toHex(callData) : callData!
}
onClose={() => setOpen(false)}
/>
</Suspense>
<DialogHeader>
<DialogTitle>Create TX</DialogTitle>
</DialogHeader>
<DialogBody className="flex flex-col gap-2">
<Suspense fallback="Loading…">
<SendTransactionForm
callData={
callData instanceof Uint8Array ? toHex(callData) : callData!
}
onClose={() => setOpen(false)}
/>
</Suspense>
</DialogBody>
</DialogContent>
</Dialog>
)
+47 -2
View File
@@ -1,13 +1,19 @@
import { ActionButton } from "@/components/ActionButton"
import { Spinner } from "@/components/Icons"
import { cn } from "@/lib/utils"
import { trackSignedTx, trackUnsignedTx } from "@/pages/Transactions"
import { unsafeApi$ } from "@/state/chains/chain.state"
import { selectedAccount$ } from "@/state/polkahub"
import { compactNumber } from "@polkadot-api/substrate-bindings"
import { fromHex, mergeUint8, toHex } from "@polkadot-api/utils"
import { AccountPicker } from "@polkahub/ui-components"
import { useStateObservable } from "@react-rxjs/core"
import { Binary } from "polkadot-api"
import { SelectAccountField } from "polkahub"
import {
AddressIdentity,
useAvailableAccounts,
useSelectedAccount,
} from "polkahub"
import { FC, useState } from "react"
import { firstValueFrom } from "rxjs"
import { twMerge } from "tailwind-merge"
@@ -78,7 +84,7 @@ export default function SubmitTxF(props: {
return (
<>
<ExtensionProvider />
<SelectAccountField />
<SelectAccount />
<div className="flex flex-col gap-2">
<SignAndSubmit {...props} />
<SubmitUnsigned {...props} />
@@ -86,3 +92,42 @@ export default function SubmitTxF(props: {
</>
)
}
const groupLabels: Record<string, string> = {
ledger: "Ledger",
readonly: "Read Only",
"polkadot-vault": "Vault",
walletconnect: "Wallet Connect",
}
const SelectAccount: FC<{
className?: string
}> = ({ className }) => {
const availableAccounts = useAvailableAccounts()
const [account, setAccount] = useSelectedAccount()
const groups = Object.entries(availableAccounts)
.filter(([, accounts]) => accounts.length > 0)
.map(([key, accounts]) => ({
name: groupLabels[key] ?? key,
accounts,
}))
if (!groups.length && !account) return null
return (
<AccountPicker
value={account}
onChange={setAccount}
groups={groups}
className={cn(className, "max-w-auto w-full")}
renderAddress={(account) => (
<AddressIdentity
addr={account.address}
name={account?.name}
copyable={false}
/>
)}
/>
)
}
+1
View File
@@ -216,6 +216,7 @@ const NetworkSwitchDialogContent: FC<{
{Object.entries(selectedNetwork.endpoints).map(
([rpcName, url]) => (
<ConnectionOption
key={rpcName}
value={url}
isSelected={selectedRpc === url}
name={rpcName}
+1 -1
View File
@@ -28,7 +28,7 @@ export function Transactions() {
return !nTxs ? null : (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger asChild>
<button className="fixed bottom-4 right-4 bg-polkadot/90 text-polkadot-foreground px-4 py-2 rounded-full shadow-lg hover:bg-primary hover:font-bold focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-opacity-50 flex items-center space-x-2">
<button className="fixed bottom-4 right-4 bg-polkadot/90 text-polkadot-foreground px-4 py-2 rounded-full shadow-lg hover:bg-polkadot hover:font-bold focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-opacity-50 flex items-center space-x-2">
{nOngoing > 0 && (
<>
<Loader2 className="animate-spin" size={20} />