create-openfort 0.1.10 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +24 -23
  3. package/package.json +1 -1
  4. package/template/openfort-templates/firebase/AGENTS.md +1 -1
  5. package/template/openfort-templates/firebase/package.json +3 -3
  6. package/template/openfort-templates/firebase/src/App.tsx +2 -1
  7. package/template/openfort-templates/firebase/src/integrations/openfort/providers.tsx +36 -35
  8. package/template/openfort-templates/firebase/src/lib/contracts.ts +34 -0
  9. package/template/openfort-templates/firebase/src/ui/openfort/blockchain/ActionsCard.tsx +25 -13
  10. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletCreation.tsx +108 -63
  11. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletListCard.tsx +211 -41
  12. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletPasswordSheets.tsx +45 -21
  13. package/template/openfort-templates/headless/AGENTS.md +1 -1
  14. package/template/openfort-templates/headless/package.json +2 -2
  15. package/template/openfort-templates/headless/src/components/cards/actions.tsx +30 -21
  16. package/template/openfort-templates/headless/src/components/cards/profile.tsx +0 -2
  17. package/template/openfort-templates/headless/src/components/cards/wallets.tsx +230 -67
  18. package/template/openfort-templates/headless/src/components/createWallet.tsx +115 -73
  19. package/template/openfort-templates/headless/src/components/passwordRecovery.tsx +48 -23
  20. package/template/openfort-templates/headless/src/components/providers.tsx +30 -25
  21. package/template/openfort-templates/headless/src/lib/contracts.ts +43 -0
  22. package/template/openfort-templates/openfort-ui/AGENTS.md +1 -1
  23. package/template/openfort-templates/openfort-ui/package.json +3 -3
  24. package/template/openfort-templates/openfort-ui/src/components/cards/actions.tsx +30 -15
  25. package/template/openfort-templates/openfort-ui/src/components/cards/auth.tsx +1 -1
  26. package/template/openfort-templates/openfort-ui/src/components/cards/wallets.tsx +232 -67
  27. package/template/openfort-templates/openfort-ui/src/components/createWallet.tsx +115 -73
  28. package/template/openfort-templates/openfort-ui/src/components/passwordRecovery.tsx +48 -23
  29. package/template/openfort-templates/openfort-ui/src/components/providers.tsx +34 -30
  30. package/template/openfort-templates/openfort-ui/src/lib/contracts.ts +35 -0
  31. package/template/openfort-templates/solana-headless/biome.json +70 -0
  32. package/template/openfort-templates/solana-headless/index.html +16 -0
  33. package/template/openfort-templates/solana-headless/package.json +34 -0
  34. package/template/openfort-templates/solana-headless/public/githubLogo.svg +5 -0
  35. package/template/openfort-templates/solana-headless/public/openfort.svg +13 -0
  36. package/template/openfort-templates/solana-headless/public/solanaLogo.svg +11 -0
  37. package/template/openfort-templates/solana-headless/src/App.tsx +7 -0
  38. package/template/openfort-templates/solana-headless/src/components/cards/auth.tsx +167 -0
  39. package/template/openfort-templates/solana-headless/src/components/cards/head.tsx +359 -0
  40. package/template/openfort-templates/solana-headless/src/components/cards/history.tsx +134 -0
  41. package/template/openfort-templates/solana-headless/src/components/cards/main.tsx +140 -0
  42. package/template/openfort-templates/solana-headless/src/components/cards/profile.tsx +80 -0
  43. package/template/openfort-templates/solana-headless/src/components/cards/send.tsx +242 -0
  44. package/template/openfort-templates/solana-headless/src/components/cards/sign.tsx +48 -0
  45. package/template/openfort-templates/solana-headless/src/components/cards/wallets.tsx +199 -0
  46. package/template/openfort-templates/solana-headless/src/components/createWallet.tsx +117 -0
  47. package/template/openfort-templates/solana-headless/src/components/passwordRecovery.tsx +167 -0
  48. package/template/openfort-templates/solana-headless/src/components/providers.tsx +23 -0
  49. package/template/openfort-templates/solana-headless/src/components/ui/Sheet.tsx +47 -0
  50. package/template/openfort-templates/solana-headless/src/components/ui/Tabs.tsx +111 -0
  51. package/template/openfort-templates/solana-headless/src/components/ui/TruncateData.tsx +31 -0
  52. package/template/openfort-templates/solana-headless/src/hooks/useSolanaMessageSigner.ts +37 -0
  53. package/template/openfort-templates/solana-headless/src/index.css +180 -0
  54. package/template/openfort-templates/solana-headless/src/lib/errors.ts +4 -0
  55. package/template/openfort-templates/solana-headless/src/lib/solana/balance.ts +17 -0
  56. package/template/openfort-templates/solana-headless/src/lib/solana/index.ts +4 -0
  57. package/template/openfort-templates/solana-headless/src/lib/solana/kora.ts +137 -0
  58. package/template/openfort-templates/solana-headless/src/lib/solana/transaction.ts +146 -0
  59. package/template/openfort-templates/solana-headless/src/lib/solana/transactionHistory.ts +39 -0
  60. package/template/openfort-templates/solana-headless/src/main.tsx +13 -0
  61. package/template/openfort-templates/solana-headless/src/vite-env.d.ts +1 -0
  62. package/template/openfort-templates/solana-headless/tsconfig.app.json +24 -0
  63. package/template/openfort-templates/solana-headless/tsconfig.json +7 -0
  64. package/template/openfort-templates/solana-headless/tsconfig.node.json +22 -0
  65. package/template/openfort-templates/solana-headless/vite.config.ts +8 -0
@@ -2,13 +2,17 @@ import {
2
2
  FingerPrintIcon,
3
3
  KeyIcon,
4
4
  LockClosedIcon,
5
+ WalletIcon,
5
6
  } from '@heroicons/react/24/outline'
6
- import { RecoveryMethod, useWallets } from '@openfort/react'
7
+ import { AccountTypeEnum, RecoveryMethod } from '@openfort/react'
8
+ import { useEthereumEmbeddedWallet } from '@openfort/react/ethereum'
7
9
  import { useState } from 'react'
8
10
 
9
11
  import { Sheet } from '../../../components/ui/Sheet'
10
12
  import { CreateWalletPasswordSheet } from './WalletPasswordSheets'
11
13
 
14
+ type CreateStep = 'choose-account-type' | 'choose-recovery-method'
15
+
12
16
  type CreateWalletSheetProps = {
13
17
  open: boolean
14
18
  onClose: () => void
@@ -23,9 +27,7 @@ export function CreateWalletSheet({
23
27
  return (
24
28
  <Sheet
25
29
  open={open}
26
- onClose={() => {
27
- onClose()
28
- }}
30
+ onClose={onClose}
29
31
  title="Create Wallet"
30
32
  description="Please choose a recovery method for your new wallet."
31
33
  >
@@ -44,83 +46,126 @@ export function CreateWallet({
44
46
  }: {
45
47
  onWalletCreated?: () => void
46
48
  }) {
49
+ const { create, status } = useEthereumEmbeddedWallet()
50
+ const [error, setError] = useState<string | null>(null)
51
+ const [step, setStep] = useState<CreateStep>('choose-account-type')
52
+ const [accountType, setAccountType] = useState<AccountTypeEnum>(AccountTypeEnum.SMART_ACCOUNT)
47
53
  const [passwordSheetOpen, setPasswordSheetOpen] = useState(false)
48
54
 
49
- const { isCreating, createWallet, error } = useWallets({
50
- onSuccess: () => {
51
- onWalletCreated?.()
52
- },
53
- })
55
+ const isCreating = status === 'creating'
54
56
 
55
57
  if (isCreating) {
56
58
  return <div>Creating wallet...</div>
57
59
  }
58
60
 
61
+ const handleCreate = async (recoveryMethod: RecoveryMethod, password?: string) => {
62
+ try {
63
+ await create({ recoveryMethod, accountType, ...(password && { password }) })
64
+ onWalletCreated?.()
65
+ } catch (err) {
66
+ setError(err instanceof Error ? err.message : 'Failed to create wallet')
67
+ }
68
+ }
69
+
59
70
  return (
60
71
  <>
61
- <div className="flex flex-col gap-4 wallet-option-group mb-4">
62
- <button
63
- className="wallet-option cursor-pointer"
64
- onClick={() =>
65
- createWallet({
66
- recovery: {
67
- recoveryMethod: RecoveryMethod.PASSKEY,
68
- },
69
- })
70
- }
71
- >
72
- <FingerPrintIcon />
73
- <div className="flex flex-col text-start">
74
- <h4>Passkey</h4>
75
- <p className="text-sm hover-description">
76
- Secure your wallet with biometric authentication.
77
- </p>
78
- </div>
79
- </button>
80
- <button
81
- className="wallet-option cursor-pointer"
82
- onClick={() =>
83
- createWallet({
84
- recovery: {
85
- recoveryMethod: RecoveryMethod.AUTOMATIC,
86
- },
87
- })
88
- }
89
- >
90
- <LockClosedIcon />
91
- <div className="flex flex-col text-start">
92
- <h4>Automatic recovery</h4>
93
- <p className="text-sm hover-description">
94
- Uses encryption session to recover your wallet.
95
- </p>
96
- </div>
97
- </button>
98
- <button
99
- className="wallet-option cursor-pointer"
100
- onClick={() => setPasswordSheetOpen(true)}
101
- >
102
- <KeyIcon />
103
- <div className="flex flex-col text-start">
104
- <h4>Password</h4>
105
- <p className="text-sm hover-description">
106
- Create a strong password to secure your wallet.
107
- </p>
72
+ {step === 'choose-account-type' && (
73
+ <div className="flex flex-col gap-4 wallet-option-group mb-4">
74
+ <p className="text-sm text-zinc-400">Select account type:</p>
75
+ {Object.values(AccountTypeEnum).map((type) => (
76
+ <button
77
+ key={type}
78
+ type="button"
79
+ className="wallet-option cursor-pointer"
80
+ onClick={() => {
81
+ setAccountType(type)
82
+ setStep('choose-recovery-method')
83
+ }}
84
+ >
85
+ <WalletIcon className="h-6 w-6 shrink-0" />
86
+ <div className="flex flex-col text-start">
87
+ <h4>{type}</h4>
88
+ <p className="text-sm hover-description">
89
+ {type === AccountTypeEnum.EOA && 'A standard externally owned account.'}
90
+ {type === AccountTypeEnum.SMART_ACCOUNT &&
91
+ 'A smart contract wallet (also creates an EOA owner).'}
92
+ {type === AccountTypeEnum.DELEGATED_ACCOUNT && 'A delegated smart account.'}
93
+ </p>
94
+ </div>
95
+ </button>
96
+ ))}
97
+ </div>
98
+ )}
99
+
100
+ {step === 'choose-recovery-method' && (
101
+ <>
102
+ <button
103
+ type="button"
104
+ className="text-xs text-zinc-400 mb-4 hover:text-zinc-200 flex items-center gap-1"
105
+ onClick={() => setStep('choose-account-type')}
106
+ >
107
+ ← Back ·{' '}
108
+ <span className="text-zinc-300">{accountType}</span>
109
+ </button>
110
+ <div className="flex flex-col gap-4 wallet-option-group mb-4">
111
+ <p className="text-sm text-zinc-400">Select recovery method:</p>
112
+ <button
113
+ type="button"
114
+ className="wallet-option cursor-pointer"
115
+ onClick={() => handleCreate(RecoveryMethod.PASSKEY)}
116
+ >
117
+ <FingerPrintIcon />
118
+ <div className="flex flex-col text-start">
119
+ <h4>Passkey</h4>
120
+ <p className="text-sm hover-description">
121
+ Secure your wallet with biometric authentication.
122
+ </p>
123
+ </div>
124
+ </button>
125
+ <button
126
+ type="button"
127
+ className="wallet-option cursor-pointer"
128
+ onClick={() => handleCreate(RecoveryMethod.AUTOMATIC)}
129
+ >
130
+ <LockClosedIcon />
131
+ <div className="flex flex-col text-start">
132
+ <h4>Automatic recovery</h4>
133
+ <p className="text-sm hover-description">
134
+ Uses encryption session to recover your wallet.
135
+ </p>
136
+ </div>
137
+ </button>
138
+ <button
139
+ type="button"
140
+ className="wallet-option cursor-pointer"
141
+ onClick={() => setPasswordSheetOpen(true)}
142
+ >
143
+ <KeyIcon />
144
+ <div className="flex flex-col text-start">
145
+ <h4>Password</h4>
146
+ <p className="text-sm hover-description">
147
+ Create a strong password to secure your wallet.
148
+ </p>
149
+ </div>
150
+ </button>
108
151
  </div>
109
- </button>
110
- </div>
111
- {error && (
112
- <p className="text-red-500 text-sm mb-2">Error: {error.message}</p>
152
+ </>
113
153
  )}
154
+
155
+ {error && <p className="text-red-500 text-sm mb-2">Error: {error}</p>}
156
+
114
157
  <p className="mb-4 text-xs text-zinc-400">
115
158
  Disclaimer: This is a demo of Openfort recovery methods. In production,
116
159
  it's best to choose one method for a smoother user experience.
117
160
  </p>
161
+
118
162
  <CreateWalletPasswordSheet
119
163
  open={passwordSheetOpen}
120
164
  onClose={() => setPasswordSheetOpen(false)}
121
- onCreateWallet={() => {
122
- onWalletCreated?.()
123
- }}
165
+ create={create}
166
+ status={status}
167
+ accountType={accountType}
168
+ onCreateWallet={onWalletCreated}
124
169
  />
125
170
  </>
126
171
  )
@@ -1,21 +1,34 @@
1
1
  import {
2
+ CheckIcon,
3
+ ChevronDownIcon,
4
+ ChevronUpIcon,
5
+ ClipboardDocumentIcon,
2
6
  FingerPrintIcon,
3
7
  KeyIcon,
4
8
  LockClosedIcon,
5
9
  } from '@heroicons/react/24/outline'
6
10
  import {
11
+ AccountTypeEnum,
7
12
  RecoveryMethod,
8
- type UserWallet,
13
+ type ConnectedEmbeddedEthereumWallet,
9
14
  useSignOut,
10
15
  useUser,
11
- useWallets,
12
16
  } from '@openfort/react'
13
- import { useState } from 'react'
17
+ import { useEthereumEmbeddedWallet } from '@openfort/react/ethereum'
18
+ import { useMemo, useState } from 'react'
14
19
  import { useAccount } from 'wagmi'
15
20
  import { CreateWallet, CreateWalletSheet } from './WalletCreation'
16
21
  import { WalletRecoverPasswordSheet } from './WalletPasswordSheets'
17
22
 
18
- function WalletRecoveryBadge({ wallet }: { wallet: UserWallet }) {
23
+ const ACCOUNT_TYPE_LABELS: Record<AccountTypeEnum, string> = {
24
+ [AccountTypeEnum.EOA]: 'EOA',
25
+ [AccountTypeEnum.SMART_ACCOUNT]: 'SM',
26
+ [AccountTypeEnum.DELEGATED_ACCOUNT]: 'DE',
27
+ }
28
+
29
+ const VISIBLE_WALLET_COUNT = 4
30
+
31
+ function WalletRecoveryBadge({ wallet }: { wallet: ConnectedEmbeddedEthereumWallet }) {
19
32
  let Icon = LockClosedIcon
20
33
  let label = 'Unknown'
21
34
 
@@ -42,28 +55,128 @@ function WalletRecoveryBadge({ wallet }: { wallet: UserWallet }) {
42
55
  )
43
56
  }
44
57
 
58
+ function WalletItem({
59
+ wallet,
60
+ isActive,
61
+ isConnecting,
62
+ nested,
63
+ onClick,
64
+ }: {
65
+ wallet: ConnectedEmbeddedEthereumWallet
66
+ isActive: boolean
67
+ isConnecting: boolean
68
+ nested?: boolean
69
+ onClick: () => void
70
+ }) {
71
+ return (
72
+ <div className={nested ? 'ml-5 flex items-center gap-1' : undefined}>
73
+ {nested && <span className="text-zinc-500 text-sm">↳</span>}
74
+ <button
75
+ key={wallet.id + wallet.address}
76
+ className="px-4 py-3 border data-[active=true]:border-zinc-300 border-zinc-700 rounded data-[active=false]:cursor-pointer data-[active=false]:hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1 text-sm w-full"
77
+ onClick={onClick}
78
+ data-active={isActive}
79
+ disabled={isActive || isConnecting}
80
+ type="button"
81
+ >
82
+ {isConnecting && isActive ? (
83
+ <p>Connecting...</p>
84
+ ) : (
85
+ <div className="flex justify-between items-center">
86
+ <p className="font-medium mr-2">
87
+ {`${wallet.address.substring(0, 6)}...${wallet.address.substring(wallet.address.length - 4)}`}
88
+ </p>
89
+ <div className="flex items-center gap-2">
90
+ {wallet.accountType && (
91
+ <span className="text-[10px] font-semibold leading-none border border-zinc-600 rounded px-1 py-0.5 opacity-70">
92
+ {ACCOUNT_TYPE_LABELS[wallet.accountType]}
93
+ </span>
94
+ )}
95
+ <WalletRecoveryBadge wallet={wallet} />
96
+ </div>
97
+ </div>
98
+ )}
99
+ </button>
100
+ </div>
101
+ )
102
+ }
103
+
45
104
  export function WalletListCard() {
46
105
  const {
47
106
  wallets,
48
- isLoadingWallets,
49
- availableWallets,
50
- setActiveWallet,
51
- isConnecting,
52
- } = useWallets()
107
+ status,
108
+ activeWallet,
109
+ setActive,
110
+ exportPrivateKey,
111
+ } = useEthereumEmbeddedWallet()
112
+ const isLoadingWallets = status === 'fetching-wallets'
113
+ const isConnecting = status === 'connecting'
53
114
  const { user, isAuthenticated } = useUser()
54
115
  const { isConnected } = useAccount()
55
116
  const { signOut } = useSignOut()
56
117
 
57
118
  const [createWalletSheetOpen, setCreateWalletSheetOpen] = useState(false)
58
- const [walletToRecover, setWalletToRecover] = useState<UserWallet | null>(
59
- null,
60
- )
119
+ const [walletToRecover, setWalletToRecover] =
120
+ useState<ConnectedEmbeddedEthereumWallet | null>(null)
121
+ const [showAllWallets, setShowAllWallets] = useState(false)
122
+ const [exportedKey, setExportedKey] = useState<string | null>(null)
123
+ const [isExporting, setIsExporting] = useState(false)
124
+ const [exportError, setExportError] = useState<string | null>(null)
125
+ const [copied, setCopied] = useState(false)
126
+
127
+ // Group wallets: EOAs at top level, Smart/Delegated accounts nested under their owner
128
+ const { topLevel, childrenByOwner } = useMemo(() => {
129
+ const ownerAddresses = new Set(wallets.map((w) => w.address.toLowerCase()))
130
+ const childrenByOwner = new Map<string, ConnectedEmbeddedEthereumWallet[]>()
131
+ const topLevel: ConnectedEmbeddedEthereumWallet[] = []
132
+
133
+ for (const wallet of wallets) {
134
+ const owner = wallet.ownerAddress?.toLowerCase()
135
+ if (owner && ownerAddresses.has(owner) && owner !== wallet.address.toLowerCase()) {
136
+ const existing = childrenByOwner.get(owner) ?? []
137
+ existing.push(wallet)
138
+ childrenByOwner.set(owner, existing)
139
+ } else {
140
+ topLevel.push(wallet)
141
+ }
142
+ }
143
+
144
+ return { topLevel, childrenByOwner }
145
+ }, [wallets])
146
+
147
+ const handleExportKey = async () => {
148
+ setIsExporting(true)
149
+ setExportError(null)
150
+ setExportedKey(null)
151
+ try {
152
+ const key = await exportPrivateKey()
153
+ setExportedKey(key)
154
+ } catch {
155
+ setExportError('Cannot export private key for this wallet.')
156
+ } finally {
157
+ setIsExporting(false)
158
+ }
159
+ }
160
+
161
+ const handleWalletClick = (wallet: ConnectedEmbeddedEthereumWallet) => {
162
+ const isActive =
163
+ activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
164
+ if (isActive || isConnecting) return
165
+
166
+ if (wallet.recoveryMethod === RecoveryMethod.PASSWORD) {
167
+ setWalletToRecover(wallet)
168
+ return
169
+ }
170
+
171
+ setActive({ address: wallet.address })
172
+ }
61
173
 
174
+ if (!activeWallet && isConnecting) return <div>recovering ...</div>
62
175
  if (isLoadingWallets || (!user && isAuthenticated)) {
63
176
  return <div>Loading wallets...</div>
64
177
  }
65
178
 
66
- if (availableWallets.length === 0) {
179
+ if (wallets.length === 0) {
67
180
  return (
68
181
  <div className="flex gap-2 flex-col w-full">
69
182
  <h1>Create a wallet</h1>
@@ -73,19 +186,9 @@ export function WalletListCard() {
73
186
  )
74
187
  }
75
188
 
76
- const handleWalletClick = (wallet: UserWallet) => {
77
- if (wallet.isActive || isConnecting) return
78
-
79
- if (wallet.recoveryMethod === RecoveryMethod.PASSWORD) {
80
- setWalletToRecover(wallet)
81
- return
82
- }
83
-
84
- setActiveWallet({
85
- walletId: 'xyz.openfort',
86
- address: wallet.address,
87
- })
88
- }
189
+ const visibleTopLevel = showAllWallets
190
+ ? topLevel
191
+ : topLevel.slice(0, VISIBLE_WALLET_COUNT)
89
192
 
90
193
  return (
91
194
  <div className="flex flex-col w-full">
@@ -97,34 +200,101 @@ export function WalletListCard() {
97
200
  <div className="space-y-4 pb-4">
98
201
  <h2>Your Wallets</h2>
99
202
  <div className="flex flex-col space-y-2">
100
- {wallets.map((wallet) => (
203
+ {visibleTopLevel.map((wallet) => {
204
+ const isActive =
205
+ activeWallet?.address.toLowerCase() === wallet.address.toLowerCase()
206
+ const children = childrenByOwner.get(wallet.address.toLowerCase())
207
+
208
+ return (
209
+ <div key={wallet.id} className="space-y-1">
210
+ <WalletItem
211
+ wallet={wallet}
212
+ isActive={isActive}
213
+ isConnecting={isConnecting}
214
+ onClick={() => handleWalletClick(wallet)}
215
+ />
216
+ {children?.map((child) => {
217
+ const isChildActive =
218
+ activeWallet?.address.toLowerCase() === child.address.toLowerCase()
219
+ return (
220
+ <WalletItem
221
+ key={child.id}
222
+ wallet={child}
223
+ isActive={isChildActive}
224
+ isConnecting={isConnecting}
225
+ nested
226
+ onClick={() => handleWalletClick(child)}
227
+ />
228
+ )
229
+ })}
230
+ </div>
231
+ )
232
+ })}
233
+
234
+ {topLevel.length > VISIBLE_WALLET_COUNT && (
101
235
  <button
102
- key={wallet.id + wallet.address}
103
- className="px-4 py-3 border data-[active=true]:border-zinc-300 border-zinc-700 rounded data-[active=false]:cursor-pointer data-[active=false]:hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1 text-sm"
104
- onClick={() => handleWalletClick(wallet)}
105
- data-active={wallet.isActive}
106
- disabled={wallet.isActive || isConnecting}
236
+ type="button"
237
+ className="flex items-center justify-center gap-1 text-xs text-zinc-400 hover:text-zinc-200 cursor-pointer transition-colors py-1"
238
+ onClick={() => setShowAllWallets((prev) => !prev)}
107
239
  >
108
- {wallet.isConnecting ? (
109
- <p>Connecting...</p>
240
+ {showAllWallets ? (
241
+ <>
242
+ Show less <ChevronUpIcon className="h-4 w-4" />
243
+ </>
110
244
  ) : (
111
- <div className="flex justify-between items-center">
112
- <p className="font-medium mr-2">
113
- {`${wallet.address.substring(0, 6)}...${wallet.address.substring(wallet.address.length - 4)}`}
114
- </p>
115
- <WalletRecoveryBadge wallet={wallet} />
116
- </div>
245
+ <>
246
+ Show {topLevel.length - VISIBLE_WALLET_COUNT} more{' '}
247
+ <ChevronDownIcon className="h-4 w-4" />
248
+ </>
117
249
  )}
118
250
  </button>
119
- ))}
251
+ )}
120
252
 
121
253
  <button
122
254
  className="p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors flex-1"
123
255
  onClick={() => setCreateWalletSheetOpen(true)}
256
+ type="button"
124
257
  >
125
258
  + Create Wallet
126
259
  </button>
127
260
  </div>
261
+
262
+ {activeWallet && (
263
+ <div className="mt-4 space-y-2">
264
+ <button
265
+ type="button"
266
+ className="w-full p-3 border border-zinc-700 rounded cursor-pointer hover:bg-zinc-700/20 hover:border-zinc-300 transition-colors text-sm flex items-center justify-center gap-2"
267
+ onClick={handleExportKey}
268
+ disabled={isExporting}
269
+ >
270
+ <KeyIcon className="h-4 w-4" />
271
+ {isExporting ? 'Exporting...' : 'Export Private Key'}
272
+ </button>
273
+ {exportError && (
274
+ <p className="text-red-400 text-xs">{exportError}</p>
275
+ )}
276
+ {exportedKey && (
277
+ <div className="flex items-center gap-2 p-3 border border-zinc-700 rounded bg-zinc-800 text-xs break-all font-mono">
278
+ <span className="flex-1">{exportedKey}</span>
279
+ <button
280
+ type="button"
281
+ className="shrink-0 p-1 rounded hover:bg-zinc-700 transition-colors cursor-pointer"
282
+ onClick={() => {
283
+ navigator.clipboard.writeText(exportedKey)
284
+ setCopied(true)
285
+ setTimeout(() => setCopied(false), 2000)
286
+ }}
287
+ >
288
+ {copied ? (
289
+ <CheckIcon className="h-4 w-4 text-green-400" />
290
+ ) : (
291
+ <ClipboardDocumentIcon className="h-4 w-4 text-zinc-400" />
292
+ )}
293
+ </button>
294
+ </div>
295
+ )}
296
+ </div>
297
+ )}
128
298
  </div>
129
299
 
130
300
  <WalletRecoverPasswordSheet