diff --git a/apps/web/src/app/app-layout.tsx b/apps/web/src/app/app-layout.tsx
index 9a40718..6b9dbe3 100644
--- a/apps/web/src/app/app-layout.tsx
+++ b/apps/web/src/app/app-layout.tsx
@@ -17,7 +17,8 @@ export function AppLayout({ children }: { children: ReactNode }) {
links={[
{ label: 'Dashboard', link: '/dashboard' },
{ label: 'Account', link: '/account' },
- { label: 'Demo', link: '/demo' },
+ { label: 'Clusters', link: '/clusters' },
+ { label: 'UI Demo', link: '/ui-demo' },
{ label: 'Dev', link: '/dev' },
]}
profile={
diff --git a/apps/web/src/app/app-routes.tsx b/apps/web/src/app/app-routes.tsx
index a810ba6..13c115e 100644
--- a/apps/web/src/app/app-routes.tsx
+++ b/apps/web/src/app/app-routes.tsx
@@ -8,6 +8,7 @@ import { DevFeature } from './features/dev/dev-feature'
const AccountList = lazy(() => import('./features/account/account-list-feature'))
const AccountDetail = lazy(() => import('./features/account/account-detail-feature'))
const ClusterFeature = lazy(() => import('./features/cluster/cluster-feature'))
+const KeypairFeature = lazy(() => import('./features/keypair/keypair-feature'))
const routes: RouteObject[] = [
{ path: '/', element: },
@@ -15,8 +16,9 @@ const routes: RouteObject[] = [
{ path: '/account/:address', element: },
{ path: '/clusters', element: },
{ path: '/dashboard', element: },
- { path: '/demo/*', element: },
+ { path: '/ui-demo/*', element: },
{ path: '/dev', element: },
+ { path: '/keypairs', element: },
{ path: '*', element: },
]
diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx
index 543aa3b..ce5e98e 100644
--- a/apps/web/src/app/app.tsx
+++ b/apps/web/src/app/app.tsx
@@ -4,6 +4,7 @@ import { AppLayout } from './app-layout'
import { AppRoutes, ThemeLink } from './app-routes'
import { ClusterProvider } from './features/cluster/cluster-data-access'
import { SolanaProvider } from './features/solana/solana-provider'
+import { KeypairProvider } from './features/keypair/keypair-data-access'
const client = new QueryClient()
@@ -11,13 +12,15 @@ export function App() {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
)
diff --git a/apps/web/src/app/features/dashboard/dashboard-feature.tsx b/apps/web/src/app/features/dashboard/dashboard-feature.tsx
index 46d6d5e..cfc1be2 100644
--- a/apps/web/src/app/features/dashboard/dashboard-feature.tsx
+++ b/apps/web/src/app/features/dashboard/dashboard-feature.tsx
@@ -1,11 +1,18 @@
-import { UiCard, UiContainer } from '@pubkey-ui/core'
+import { UiContainer, UiDashboardGrid } from '@pubkey-ui/core'
+import { IconApps, IconBug, IconKey, IconListDetails, IconServer } from '@tabler/icons-react'
export function DashboardFeature() {
return (
-
- GM
-
+
)
}
diff --git a/apps/web/src/app/features/keypair/keypair-data-access.tsx b/apps/web/src/app/features/keypair/keypair-data-access.tsx
new file mode 100644
index 0000000..48295a5
--- /dev/null
+++ b/apps/web/src/app/features/keypair/keypair-data-access.tsx
@@ -0,0 +1,86 @@
+import { Keypair as SolanaKeypair } from '@solana/web3.js'
+import { atom, useAtomValue, useSetAtom } from 'jotai'
+import { atomWithStorage } from 'jotai/utils'
+import { createContext, ReactNode, useContext } from 'react'
+import { ellipsify } from '../account/account-ui'
+
+export interface Keypair {
+ name: string
+ publicKey: string
+ secretKey: string
+ active?: boolean
+ solana?: SolanaKeypair
+}
+
+export const defaultKeypairs: Keypair[] = []
+
+const keypairAtom = atomWithStorage('solana-keypair', defaultKeypairs[0])
+const keypairsAtom = atomWithStorage('solana-keypairs', defaultKeypairs)
+
+const activeKeypairsAtom = atom((get) => {
+ const keypairs = get(keypairsAtom)
+ const keypair = get(keypairAtom)
+ return keypairs.map((item) => ({
+ ...item,
+ active: item?.name === keypair?.name,
+ }))
+})
+
+const activeKeypairAtom = atom((get) => {
+ const keypairs = get(activeKeypairsAtom)
+
+ return keypairs.find((item) => item.active) || keypairs[0]
+})
+
+export interface KeypairProviderContext {
+ keypair: Keypair
+ keypairs: Keypair[]
+ addKeypair: (keypair: Keypair) => void
+ deleteKeypair: (keypair: Keypair) => void
+ setKeypair: (keypair: Keypair) => void
+ generateKeypair: () => void
+}
+
+const Context = createContext({} as KeypairProviderContext)
+
+export function KeypairProvider({ children }: { children: ReactNode }) {
+ const keypair = useAtomValue(activeKeypairAtom)
+ const keypairs = useAtomValue(activeKeypairsAtom)
+ const setKeypair = useSetAtom(keypairAtom)
+ const setKeypairs = useSetAtom(keypairsAtom)
+
+ function addNewKeypair(kp: SolanaKeypair) {
+ const keypair: Keypair = {
+ name: ellipsify(kp.publicKey.toString()),
+ publicKey: kp.publicKey.toString(),
+ secretKey: `[${kp.secretKey.join(',')}]`,
+ }
+ setKeypairs([...keypairs, keypair])
+ if (!keypairs.length) {
+ activateKeypair(keypair)
+ }
+ }
+
+ function activateKeypair(keypair: Keypair) {
+ const kp = SolanaKeypair.fromSecretKey(new Uint8Array(JSON.parse(keypair.secretKey)))
+ setKeypair({ ...keypair, solana: kp })
+ }
+
+ const value: KeypairProviderContext = {
+ keypair,
+ keypairs: keypairs.sort((a, b) => (a.name > b.name ? 1 : -1)),
+ addKeypair: (keypair: Keypair) => {
+ setKeypairs([...keypairs, keypair])
+ },
+ deleteKeypair: (keypair: Keypair) => {
+ setKeypairs(keypairs.filter((item) => item.name !== keypair.name))
+ },
+ setKeypair: (keypair: Keypair) => activateKeypair(keypair),
+ generateKeypair: () => addNewKeypair(SolanaKeypair.generate()),
+ }
+ return {children}
+}
+
+export function useKeypair() {
+ return useContext(Context)
+}
diff --git a/apps/web/src/app/features/keypair/keypair-feature.tsx b/apps/web/src/app/features/keypair/keypair-feature.tsx
new file mode 100644
index 0000000..6be1fee
--- /dev/null
+++ b/apps/web/src/app/features/keypair/keypair-feature.tsx
@@ -0,0 +1,24 @@
+import { Button, Container, Group, Text, Title } from '@mantine/core'
+import { UiStack } from '@pubkey-ui/core'
+
+import { KeypairUiModal, KeypairUiTable } from './keypair-ui'
+import { useKeypair } from './keypair-data-access'
+
+export default function KeypairFeature() {
+ const { generateKeypair } = useKeypair()
+ return (
+
+
+
+ Keypairs
+ Manage and select your Solana keypairs
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/app/features/keypair/keypair-ui.tsx b/apps/web/src/app/features/keypair/keypair-ui.tsx
new file mode 100644
index 0000000..d4a805b
--- /dev/null
+++ b/apps/web/src/app/features/keypair/keypair-ui.tsx
@@ -0,0 +1,86 @@
+import { ActionIcon, Anchor, Button, Group, Modal, Table, Text, TextInput } from '@mantine/core'
+import { useDisclosure } from '@mantine/hooks'
+import { IconCurrencySolana, IconTrash } from '@tabler/icons-react'
+import { useState } from 'react'
+import { useKeypair } from './keypair-data-access'
+import { UiAlert, UiDebugModal } from '@pubkey-ui/core'
+
+export function KeypairUiModal() {
+ const { addKeypair } = useKeypair()
+ const [opened, { close, open }] = useDisclosure(false)
+ const [name, setName] = useState('')
+
+ return (
+ <>
+
+
+ setName(e.target.value)} />
+
+
+
+ >
+ )
+}
+
+export function KeypairUiTable() {
+ const { keypairs, generateKeypair, setKeypair, deleteKeypair } = useKeypair()
+
+ return keypairs.length ? (
+
+
+
+ Name / Network / Endpoint
+ Actions
+
+
+
+ {keypairs?.map((item) => (
+
+
+
+ {item?.active ? (
+ item.name
+ ) : (
+ setKeypair(item)}>
+ {item.name}
+
+ )}
+
+
+ {item.publicKey}
+
+
+
+
+
+
+
+
+ {
+ if (!window.confirm('Are you sure?')) return
+ deleteKeypair(item)
+ }}
+ >
+
+
+
+
+
+ ))}
+
+
+ ) : (
+ generateKeypair()}>Generate Keypair} />
+ )
+}
diff --git a/packages/core/src/lib/ui-form/ui-form-field.ts b/packages/core/src/lib/ui-form/ui-form-field.ts
index 0fd286e..8b553cf 100644
--- a/packages/core/src/lib/ui-form/ui-form-field.ts
+++ b/packages/core/src/lib/ui-form/ui-form-field.ts
@@ -16,6 +16,7 @@ export interface UiFormField {
placeholder?: string
required?: boolean
readOnly?: boolean
+ multiple?: boolean
disabled?: boolean
rows?: number
type: UiFormFieldType
diff --git a/packages/core/src/lib/ui-form/ui-form-select.tsx b/packages/core/src/lib/ui-form/ui-form-select.tsx
index cc0fbe9..194e319 100644
--- a/packages/core/src/lib/ui-form/ui-form-select.tsx
+++ b/packages/core/src/lib/ui-form/ui-form-select.tsx
@@ -1,6 +1,6 @@
import { UiFormField, UiFormFieldType } from './ui-form-field'
-export type UiFormSelect = Omit, 'key' | 'rows' | 'type'>
+export type UiFormSelect = Omit, 'key' | 'rows' | 'type'> & { multiple?: boolean }
export function formFieldSelect(key: keyof T, options: UiFormSelect): UiFormField {
return {
diff --git a/packages/core/src/lib/ui-form/ui-form.tsx b/packages/core/src/lib/ui-form/ui-form.tsx
index 90ffba5..64759e3 100644
--- a/packages/core/src/lib/ui-form/ui-form.tsx
+++ b/packages/core/src/lib/ui-form/ui-form.tsx
@@ -134,6 +134,7 @@ export function UiForm({
key={field.key?.toString()}
description={field.description}
label={field.label}
+ multiple={field.multiple}
placeholder={field.placeholder ?? field.label}
required={field.required}
data={field.options ?? []}