feat: manage read-only accounts
This commit is contained in:
@@ -1,190 +1,9 @@
|
||||
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 {
|
||||
accountDetail$,
|
||||
accounts$,
|
||||
getPublicKey,
|
||||
} from "@/state/extension-accounts.state"
|
||||
import { identity$, isVerified } from "@/state/identity.state"
|
||||
import { cn } from "@/utils/cn"
|
||||
import { AccountIdInput } from "@/components/AccountIdInput"
|
||||
import { EditAccountId, NOTIN } from "@polkadot-api/react-builder"
|
||||
import { getSs58AddressInfo } 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 { map, switchMap, take } from "rxjs"
|
||||
|
||||
const hintedAccounts$ = state(
|
||||
combineKeys(
|
||||
accounts$.pipe(
|
||||
map((accounts) =>
|
||||
[...accounts.entries()]
|
||||
.filter(([, { address }]) => !address.startsWith("0x"))
|
||||
.map(([key]) => key),
|
||||
),
|
||||
),
|
||||
(address) =>
|
||||
accounts$.pipe(
|
||||
map((v) => v.get(address)!),
|
||||
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 }) => (
|
||||
<AccountIdInput
|
||||
value={value === NOTIN ? null : value}
|
||||
onValueChanged={(v) => onValueChanged(v === null ? NOTIN : v)}
|
||||
/>
|
||||
)
|
||||
|
||||
export const CAccountId: EditAccountId = ({ value, onValueChanged }) => {
|
||||
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 !== NOTIN &&
|
||||
!accounts.has(toHex(getPublicKey(value) ?? new Uint8Array()))
|
||||
|
||||
const accountList = Array.from(accounts.entries())
|
||||
if (value !== NOTIN) {
|
||||
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="flex w-64 justify-between overflow-hidden px-2 border border-border bg-input"
|
||||
forceSvgSize={false}
|
||||
>
|
||||
{value !== NOTIN ? (
|
||||
<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…"
|
||||
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 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"
|
||||
forceSvgSize={false}
|
||||
>
|
||||
<AccountIdDisplay value={account} className="overflow-hidden" />
|
||||
<Check
|
||||
size={12}
|
||||
className={cn(
|
||||
"ml-auto shrink-0",
|
||||
selected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
)
|
||||
}
|
||||
|
||||
208
src/components/AccountIdInput.tsx
Normal file
208
src/components/AccountIdInput.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
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 { groupBy } from "@/lib/groupBy"
|
||||
import { accounts$ } from "@/state/accounts.state"
|
||||
import { getPublicKey } from "@/state/extension-accounts.state"
|
||||
import { identity$, isVerified } from "@/state/identity.state"
|
||||
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$ = accounts$.pipeState(
|
||||
map((accounts) => groupBy(accounts, (acc) => acc.accountId)),
|
||||
map((groups) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(groups)
|
||||
.filter(([accountId]) => !accountId.startsWith("0x"))
|
||||
.map(([accountId, [groupRep]]) => [accountId, groupRep]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
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.accountId).pipe(
|
||||
map((identity) => ({
|
||||
address: details.accountId,
|
||||
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,
|
||||
)}
|
||||
forceSvgSize={false}
|
||||
>
|
||||
{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"
|
||||
forceSvgSize={false}
|
||||
>
|
||||
<AccountIdDisplay value={account} className="overflow-hidden" />
|
||||
<Check
|
||||
size={12}
|
||||
className={cn(
|
||||
"ml-auto shrink-0",
|
||||
selected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { state } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { useState } from "react"
|
||||
import { map } from "rxjs"
|
||||
import { getHashParams, setHashParams } from "../hashParams"
|
||||
|
||||
interface Parser<T> {
|
||||
@@ -89,3 +92,31 @@ const sessionHashState: ExternalState = {
|
||||
},
|
||||
}
|
||||
export const useHashSessionState = createExternalStateHook(sessionHashState)
|
||||
|
||||
export const createLocalStorageState = <T>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
serializer: {
|
||||
stringify: (value: Exclude<T, null>) => string
|
||||
parse: (value: string) => T | null
|
||||
} = JSON,
|
||||
) => {
|
||||
const [valueChange$, setValue] = createSignal<T | null>()
|
||||
valueChange$.subscribe((v) =>
|
||||
v === null
|
||||
? localStorage.removeItem(key)
|
||||
: localStorage.setItem(key, serializer.stringify(v as Exclude<T, null>)),
|
||||
)
|
||||
|
||||
const state$ = state(
|
||||
() => valueChange$.pipe(map((v) => (v === null ? defaultValue : v))),
|
||||
() => {
|
||||
const initialValueStr = localStorage.getItem(key)
|
||||
return initialValueStr != null
|
||||
? (serializer.parse(initialValueStr) ?? defaultValue)
|
||||
: defaultValue
|
||||
},
|
||||
)
|
||||
|
||||
return [state$(), setValue] as const
|
||||
}
|
||||
|
||||
@@ -58,8 +58,9 @@ const AccountCard: FC<{
|
||||
}
|
||||
|
||||
const sourceColors: Record<AccountSource, string> = {
|
||||
extension: "var(--color-orange-800)",
|
||||
walletconnect: "var(--color-blue-800)",
|
||||
extension: "var(--color-lime-700)",
|
||||
walletconnect: "var(--color-blue-700)",
|
||||
readonly: "var(--color-orange-700)",
|
||||
}
|
||||
const getExtensionName = (id: string) => knownExtensions[id]?.name ?? id
|
||||
|
||||
@@ -155,7 +156,7 @@ const Balances: FC<{
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-col sm:flex-row justify-evenly grow">
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-x-8 grow">
|
||||
<div className="flex flex-row sm:flex-col gap-2">
|
||||
<div className="text-muted-foreground">Balance</div>
|
||||
<div>
|
||||
|
||||
83
src/pages/Accounts/AddressProvider.tsx
Normal file
83
src/pages/Accounts/AddressProvider.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { AccountIdDisplay } from "@/components/AccountIdDisplay"
|
||||
import { AccountIdInput } from "@/components/AccountIdInput"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
|
||||
import { readOnlyAddresses$, setAddresses } from "@/state/accounts.state"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import { Eye, Trash2 } from "lucide-react"
|
||||
import { SS58String } from "polkadot-api"
|
||||
import { useState } from "react"
|
||||
import { SourceButton } from "./SourceButton"
|
||||
|
||||
export const AddressProvider = () => (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<SourceButton label="Address">
|
||||
<div>
|
||||
<Eye className="size-10" />
|
||||
</div>
|
||||
</SourceButton>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<ManageAddresses />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
const ManageAddresses = () => {
|
||||
const [addressInput, setAddressInput] = useState<SS58String | null>(null)
|
||||
const readOnlyAddresses = useStateObservable(readOnlyAddresses$)
|
||||
|
||||
return (
|
||||
<div className="space-y-4 overflow-hidden">
|
||||
<form
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault()
|
||||
if (!addressInput) return
|
||||
|
||||
setAddresses([
|
||||
...readOnlyAddresses.filter((v) => v !== addressInput),
|
||||
addressInput,
|
||||
])
|
||||
setAddressInput(null)
|
||||
}}
|
||||
>
|
||||
<h3 className="font-medium text-muted-foreground">
|
||||
Add read-only address
|
||||
</h3>
|
||||
<div className="flex gap-2 items-center">
|
||||
<AccountIdInput
|
||||
className="grow"
|
||||
value={addressInput}
|
||||
onValueChanged={setAddressInput}
|
||||
/>
|
||||
<Button variant="secondary" disabled={!addressInput}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{readOnlyAddresses.length ? (
|
||||
<div>
|
||||
<h3 className="font-medium text-muted-foreground">Added addresses</h3>
|
||||
<ul className="space-y-3">
|
||||
{readOnlyAddresses.map((addr) => (
|
||||
<li key={addr} className="flex gap-2 items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-destructive border-destructive"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setAddresses(readOnlyAddresses.filter((v) => addr !== v))
|
||||
}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
<AccountIdDisplay value={addr} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Spinner } from "@/components/Icons"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
availableExtensions$,
|
||||
onToggleExtension,
|
||||
selectedExtensions$,
|
||||
} from "@/state/extension-accounts.state"
|
||||
// import { walletConnectStatus$ } from
|
||||
import { cn } from "@/utils"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { CircleQuestionMark, Eye } from "lucide-react"
|
||||
import { FC, MouseEvent, PropsWithChildren } from "react"
|
||||
import { CircleQuestionMark } from "lucide-react"
|
||||
import { FC } from "react"
|
||||
import { defer, from, switchMap } from "rxjs"
|
||||
import { AddressProvider } from "./AddressProvider"
|
||||
import { SourceButton } from "./SourceButton"
|
||||
|
||||
const lazyWalletConnectStatus$ = state(
|
||||
defer(() =>
|
||||
@@ -58,11 +57,7 @@ export const Providers = () => {
|
||||
</li>
|
||||
))}
|
||||
|
||||
{/* <SourceButton label="Address">
|
||||
<div>
|
||||
<Eye className="size-10" />
|
||||
</div>
|
||||
</SourceButton> */}
|
||||
<AddressProvider />
|
||||
{/* <SourceButton label="Ledger" disabled>
|
||||
<img
|
||||
src={import.meta.env.BASE_URL + "providers/ledger.webp"}
|
||||
@@ -134,26 +129,3 @@ const ExtensionButton: FC<{
|
||||
</SourceButton>
|
||||
)
|
||||
}
|
||||
|
||||
const SourceButton: FC<
|
||||
PropsWithChildren<{
|
||||
label: string
|
||||
isSelected?: boolean
|
||||
className?: string
|
||||
onClick?: (evt: MouseEvent) => void
|
||||
disabled?: boolean
|
||||
}>
|
||||
> = ({ label, isSelected, onClick, className, children, disabled }) => (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("h-auto min-w-40", isSelected ? "bg-accent" : "")}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
forceSvgSize={false}
|
||||
>
|
||||
{children}
|
||||
<div className="text-left">
|
||||
<span className={cn("font-bold", className)}>{label}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
|
||||
26
src/pages/Accounts/SourceButton.tsx
Normal file
26
src/pages/Accounts/SourceButton.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/utils"
|
||||
import { FC, MouseEvent, PropsWithChildren } from "react"
|
||||
|
||||
export const SourceButton: FC<
|
||||
PropsWithChildren<{
|
||||
label: string
|
||||
isSelected?: boolean
|
||||
className?: string
|
||||
onClick?: (evt: MouseEvent) => void
|
||||
disabled?: boolean
|
||||
}>
|
||||
> = ({ label, isSelected, onClick, className, children, disabled }) => (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("h-auto min-w-40", isSelected ? "bg-accent" : "")}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
forceSvgSize={false}
|
||||
>
|
||||
{children}
|
||||
<div className="text-left">
|
||||
<span className={cn("font-bold", className)}>{label}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
@@ -68,8 +68,10 @@ export const selectedAccount$ = state(
|
||||
|
||||
const groupedAccounts$ = allAccounts$.pipeState(
|
||||
map((v) =>
|
||||
groupBy(v, (acc) =>
|
||||
acc.type === "extension" ? acc.extensionId : acc.type,
|
||||
groupBy(
|
||||
// Accept only signer accounts for submitting transactions
|
||||
v.filter((v) => "signer" in v),
|
||||
(acc) => (acc.type === "extension" ? acc.extensionId : acc.type),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { createLocalStorageState } from "@/lib/externalState"
|
||||
import { state } from "@react-rxjs/core"
|
||||
import type { HexString, PolkadotSigner, SS58String } from "polkadot-api"
|
||||
import type { InjectedPolkadotAccount } from "polkadot-api/pjs-signer"
|
||||
import { combineLatest, defer, from, map, switchMap } from "rxjs"
|
||||
import { accountsByExtension$ } from "./extension-accounts.state"
|
||||
|
||||
export type AccountSource = "extension" | "walletconnect"
|
||||
export type AccountSource = "extension" | "walletconnect" | "readonly"
|
||||
export const accountSourceTypeToName: Record<AccountSource, string> = {
|
||||
extension: "Extension",
|
||||
walletconnect: "Wallet Connect",
|
||||
readonly: "Read-only",
|
||||
}
|
||||
|
||||
const lazyWalletConnectAccounts$ = defer(() =>
|
||||
@@ -69,10 +71,30 @@ export const walletConnectAccounts$ = state(
|
||||
[],
|
||||
)
|
||||
|
||||
export type Account = ExtensionAccount | WalletConnectAccount
|
||||
export const accounts$ = state(
|
||||
combineLatest([extensionAccounts$, walletConnectAccounts$]).pipe(
|
||||
map((v): Account[] => v.flat()),
|
||||
export interface ReadOnlyAccount extends BaseAccount {
|
||||
type: "readonly"
|
||||
}
|
||||
export const [readOnlyAddresses$, setAddresses] = createLocalStorageState(
|
||||
"read-only-addr",
|
||||
[] as string[],
|
||||
)
|
||||
export const readOnlyAccounts$ = readOnlyAddresses$.pipe(
|
||||
map((v) =>
|
||||
v.map(
|
||||
(address): ReadOnlyAccount => ({
|
||||
type: "readonly",
|
||||
accountId: address,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
export type Account = ExtensionAccount | WalletConnectAccount | ReadOnlyAccount
|
||||
export const accounts$ = state(
|
||||
combineLatest([
|
||||
extensionAccounts$,
|
||||
walletConnectAccounts$,
|
||||
readOnlyAccounts$,
|
||||
]).pipe(map((v): Account[] => v.flat())),
|
||||
[],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user