auramaxx 1.0.0-alpha.4

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 (363) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +112 -0
  3. package/bin/aurawallet.js +121 -0
  4. package/docs/ADAPTERS.md +467 -0
  5. package/docs/API.md +2679 -0
  6. package/docs/APPS.md +198 -0
  7. package/docs/ARCHITECTURE.md +350 -0
  8. package/docs/AUTH.md +698 -0
  9. package/docs/BEST-PRACTICES.md +121 -0
  10. package/docs/CLI.md +61 -0
  11. package/docs/DEVELOPING-APPS.md +452 -0
  12. package/docs/EXTENSION.md +97 -0
  13. package/docs/JOBS.md +33 -0
  14. package/docs/MCP.md +76 -0
  15. package/docs/PROTOCOL.md +142 -0
  16. package/docs/SETUP.md +219 -0
  17. package/docs/WORKSPACE.md +672 -0
  18. package/docs/agent-auth.md +63 -0
  19. package/docs/aura-file.md +48 -0
  20. package/docs/credentials.md +53 -0
  21. package/docs/external/getting-started.md +65 -0
  22. package/docs/external/overview.md +45 -0
  23. package/docs/external/use-cases.md +48 -0
  24. package/docs/external/why-aura.md +35 -0
  25. package/docs/jobs/connect-agent.md +77 -0
  26. package/docs/jobs/migrate-from-dotenv.md +79 -0
  27. package/docs/jobs/recover-from-lockout.md +72 -0
  28. package/docs/jobs/secure-ci.md +63 -0
  29. package/docs/oauth2.md +42 -0
  30. package/docs/passkeys.md +60 -0
  31. package/docs/security.md +540 -0
  32. package/docs/specs/aura-open-protocol.md +61 -0
  33. package/docs/specs/aura-provider-plugin.md +24 -0
  34. package/docs/specs/aura-registry-model.md +31 -0
  35. package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
  36. package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
  37. package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
  38. package/docs/specs/fixtures/valid-basic.aura +4 -0
  39. package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
  40. package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
  41. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  42. package/docs/totp.md +40 -0
  43. package/docs/wallet/AI.md +508 -0
  44. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  45. package/docs/wallet/README.md +47 -0
  46. package/docs/wallet/STRATEGY.md +89 -0
  47. package/next.config.ts +21 -0
  48. package/package.json +151 -0
  49. package/postcss.config.mjs +8 -0
  50. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  51. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  52. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  53. package/prisma/migrations/migration_lock.toml +3 -0
  54. package/prisma/schema.prisma +447 -0
  55. package/public/logo-chevron.svg +31 -0
  56. package/public/logo-concentric.svg +31 -0
  57. package/public/logo-crosshatch.svg +39 -0
  58. package/public/logo-dashed.svg +39 -0
  59. package/public/logo-horizontal.svg +31 -0
  60. package/public/logo-m56.svg +64 -0
  61. package/public/logo.webp +0 -0
  62. package/scripts/add-app.js +245 -0
  63. package/scripts/init.sh +57 -0
  64. package/scripts/migrate-apikeys-to-credentials.ts +35 -0
  65. package/scripts/sandbox-agent-flow.sh +235 -0
  66. package/scripts/sandbox.sh +175 -0
  67. package/scripts/validate-job-docs.mjs +125 -0
  68. package/server/abi/SwapHelper.json +438 -0
  69. package/server/cli/approval.ts +447 -0
  70. package/server/cli/commands/app.ts +204 -0
  71. package/server/cli/commands/cron.ts +24 -0
  72. package/server/cli/commands/doctor.ts +1007 -0
  73. package/server/cli/commands/env.ts +456 -0
  74. package/server/cli/commands/init.ts +752 -0
  75. package/server/cli/commands/mcp.ts +125 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/shell-hook.ts +468 -0
  78. package/server/cli/commands/start.ts +62 -0
  79. package/server/cli/commands/status.ts +59 -0
  80. package/server/cli/commands/stop.ts +14 -0
  81. package/server/cli/commands/token.ts +180 -0
  82. package/server/cli/commands/unlock.ts +49 -0
  83. package/server/cli/commands/vault.ts +417 -0
  84. package/server/cli/index.ts +328 -0
  85. package/server/cli/lib/aura-parser.ts +64 -0
  86. package/server/cli/lib/credential-create.ts +74 -0
  87. package/server/cli/lib/credential-resolve.ts +254 -0
  88. package/server/cli/lib/dotenv-migrate.ts +116 -0
  89. package/server/cli/lib/dotenv-parser.ts +146 -0
  90. package/server/cli/lib/http.ts +91 -0
  91. package/server/cli/lib/init-steps.ts +76 -0
  92. package/server/cli/lib/local-agent-trust.ts +45 -0
  93. package/server/cli/lib/process.ts +136 -0
  94. package/server/cli/lib/prompt.ts +85 -0
  95. package/server/cli/lib/theme.ts +240 -0
  96. package/server/cli/socket.ts +570 -0
  97. package/server/cli/transport-client.ts +50 -0
  98. package/server/cron/index.ts +137 -0
  99. package/server/cron/job.ts +31 -0
  100. package/server/cron/jobs/balance-sync.ts +436 -0
  101. package/server/cron/jobs/incoming-scan.ts +506 -0
  102. package/server/cron/jobs/native-price.ts +70 -0
  103. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  104. package/server/cron/jobs/strategy-runner.ts +175 -0
  105. package/server/cron/scheduler.ts +125 -0
  106. package/server/index.ts +406 -0
  107. package/server/lib/adapters/factory.ts +110 -0
  108. package/server/lib/adapters/index.ts +19 -0
  109. package/server/lib/adapters/router.ts +297 -0
  110. package/server/lib/adapters/telegram.ts +645 -0
  111. package/server/lib/adapters/types.ts +89 -0
  112. package/server/lib/adapters/webhook.ts +95 -0
  113. package/server/lib/address.ts +49 -0
  114. package/server/lib/agent-auth/contracts.ts +1194 -0
  115. package/server/lib/agent-profiles.ts +328 -0
  116. package/server/lib/ai.ts +285 -0
  117. package/server/lib/api-registry/contracts.ts +86 -0
  118. package/server/lib/api-registry/validation.ts +172 -0
  119. package/server/lib/apikey-migration.ts +189 -0
  120. package/server/lib/app-installer.ts +505 -0
  121. package/server/lib/app-tokens.ts +247 -0
  122. package/server/lib/auth.ts +314 -0
  123. package/server/lib/batch.ts +242 -0
  124. package/server/lib/cold.ts +874 -0
  125. package/server/lib/config.ts +381 -0
  126. package/server/lib/credential-access-audit.ts +85 -0
  127. package/server/lib/credential-access-policy.ts +110 -0
  128. package/server/lib/credential-health.ts +343 -0
  129. package/server/lib/credential-import.ts +487 -0
  130. package/server/lib/credential-scope.ts +87 -0
  131. package/server/lib/credential-shares.ts +190 -0
  132. package/server/lib/credential-transport.ts +342 -0
  133. package/server/lib/credential-vault.ts +77 -0
  134. package/server/lib/credentials.ts +333 -0
  135. package/server/lib/crypto.ts +8 -0
  136. package/server/lib/db.ts +15 -0
  137. package/server/lib/defaults.ts +366 -0
  138. package/server/lib/dex/index.ts +80 -0
  139. package/server/lib/dex/relay.ts +235 -0
  140. package/server/lib/dex/types.ts +59 -0
  141. package/server/lib/dex/uniswap.ts +370 -0
  142. package/server/lib/e2e-agent/artifacts.ts +36 -0
  143. package/server/lib/e2e-agent/contracts.ts +112 -0
  144. package/server/lib/e2e-agent/validation.ts +135 -0
  145. package/server/lib/encrypt.ts +128 -0
  146. package/server/lib/error.ts +20 -0
  147. package/server/lib/events.ts +205 -0
  148. package/server/lib/hot.ts +357 -0
  149. package/server/lib/key-fingerprint.ts +28 -0
  150. package/server/lib/logger.ts +331 -0
  151. package/server/lib/network.ts +137 -0
  152. package/server/lib/notifications.ts +219 -0
  153. package/server/lib/oauth2-refresh.ts +241 -0
  154. package/server/lib/oursecret.ts +54 -0
  155. package/server/lib/passkey-credential.ts +360 -0
  156. package/server/lib/passkey.ts +68 -0
  157. package/server/lib/permissions.ts +248 -0
  158. package/server/lib/pino.ts +24 -0
  159. package/server/lib/policy-preview.ts +138 -0
  160. package/server/lib/price.ts +338 -0
  161. package/server/lib/prices.ts +34 -0
  162. package/server/lib/project-scope.ts +239 -0
  163. package/server/lib/resolve-action.ts +427 -0
  164. package/server/lib/resolve.ts +36 -0
  165. package/server/lib/sessions.ts +632 -0
  166. package/server/lib/solana/connection.ts +26 -0
  167. package/server/lib/solana/jupiter.ts +128 -0
  168. package/server/lib/solana/transfer.ts +108 -0
  169. package/server/lib/solana/wallet.ts +136 -0
  170. package/server/lib/strategy/emits.ts +21 -0
  171. package/server/lib/strategy/engine.ts +1305 -0
  172. package/server/lib/strategy/executor.ts +115 -0
  173. package/server/lib/strategy/hook-context.ts +158 -0
  174. package/server/lib/strategy/hooks.ts +990 -0
  175. package/server/lib/strategy/index.ts +28 -0
  176. package/server/lib/strategy/installer.ts +305 -0
  177. package/server/lib/strategy/loader.ts +256 -0
  178. package/server/lib/strategy/message.ts +235 -0
  179. package/server/lib/strategy/repository.ts +218 -0
  180. package/server/lib/strategy/session-logger.ts +693 -0
  181. package/server/lib/strategy/sources.ts +288 -0
  182. package/server/lib/strategy/state.ts +189 -0
  183. package/server/lib/strategy/templates.ts +403 -0
  184. package/server/lib/strategy/tick.ts +404 -0
  185. package/server/lib/strategy/types.ts +230 -0
  186. package/server/lib/swap.ts +3 -0
  187. package/server/lib/temp.ts +86 -0
  188. package/server/lib/token-metadata.ts +86 -0
  189. package/server/lib/token-safety.ts +200 -0
  190. package/server/lib/token-search.ts +444 -0
  191. package/server/lib/totp.ts +194 -0
  192. package/server/lib/transactions.ts +123 -0
  193. package/server/lib/transport.ts +75 -0
  194. package/server/lib/txhistory/decoder.ts +262 -0
  195. package/server/lib/txhistory/enricher.ts +652 -0
  196. package/server/lib/txhistory/index.ts +391 -0
  197. package/server/lib/txhistory/signatures.ts +59 -0
  198. package/server/lib/verified-summary.ts +421 -0
  199. package/server/mcp/profile-policy.ts +30 -0
  200. package/server/mcp/server.ts +619 -0
  201. package/server/mcp/tools.ts +523 -0
  202. package/server/middleware/auth.ts +119 -0
  203. package/server/middleware/requestLogger.ts +84 -0
  204. package/server/routes/actions.ts +459 -0
  205. package/server/routes/adapters.ts +703 -0
  206. package/server/routes/addressbook.ts +113 -0
  207. package/server/routes/ai.ts +34 -0
  208. package/server/routes/apikeys.ts +295 -0
  209. package/server/routes/apps.ts +601 -0
  210. package/server/routes/auth.ts +457 -0
  211. package/server/routes/backup.ts +340 -0
  212. package/server/routes/batch.ts +270 -0
  213. package/server/routes/bookmarks.ts +162 -0
  214. package/server/routes/credential-shares.ts +198 -0
  215. package/server/routes/credential-vaults.ts +154 -0
  216. package/server/routes/credentials.ts +1290 -0
  217. package/server/routes/dashboard.ts +71 -0
  218. package/server/routes/defaults.ts +124 -0
  219. package/server/routes/fund.ts +229 -0
  220. package/server/routes/import.ts +352 -0
  221. package/server/routes/launch.ts +665 -0
  222. package/server/routes/lock.ts +54 -0
  223. package/server/routes/logs.ts +68 -0
  224. package/server/routes/nuke.ts +111 -0
  225. package/server/routes/passkey-credentials.ts +99 -0
  226. package/server/routes/passkey.ts +346 -0
  227. package/server/routes/portfolio.ts +217 -0
  228. package/server/routes/price.ts +63 -0
  229. package/server/routes/resolve.ts +31 -0
  230. package/server/routes/security.ts +45 -0
  231. package/server/routes/send-evm.ts +241 -0
  232. package/server/routes/send-solana.ts +281 -0
  233. package/server/routes/send.ts +178 -0
  234. package/server/routes/setup.ts +210 -0
  235. package/server/routes/strategy.ts +894 -0
  236. package/server/routes/swap-evm.ts +353 -0
  237. package/server/routes/swap-solana.ts +177 -0
  238. package/server/routes/swap.ts +356 -0
  239. package/server/routes/token.ts +247 -0
  240. package/server/routes/unlock.ts +403 -0
  241. package/server/routes/wallet-assets.ts +361 -0
  242. package/server/routes/wallet-transactions.ts +515 -0
  243. package/server/routes/wallet.ts +710 -0
  244. package/server/types.ts +146 -0
  245. package/skills/aurawallet/SKILL.md +739 -0
  246. package/skills/aurawallet-setup/SKILL.md +74 -0
  247. package/skills/security-review/SKILL.md +148 -0
  248. package/src/app/api/agent-requests/route.ts +30 -0
  249. package/src/app/api/apps/install/route.ts +126 -0
  250. package/src/app/api/apps/manifests/route.ts +16 -0
  251. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  252. package/src/app/api/events/route.ts +92 -0
  253. package/src/app/api/page.tsx +212 -0
  254. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  255. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  256. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  257. package/src/app/api/workspace/[id]/route.ts +168 -0
  258. package/src/app/api/workspace/auth.ts +34 -0
  259. package/src/app/api/workspace/config/route.ts +106 -0
  260. package/src/app/api/workspace/import/route.ts +127 -0
  261. package/src/app/api/workspace/route.ts +116 -0
  262. package/src/app/app/page.tsx +2122 -0
  263. package/src/app/apple-icon.png +0 -0
  264. package/src/app/docs/page.tsx +178 -0
  265. package/src/app/favicon.ico +0 -0
  266. package/src/app/globals.css +572 -0
  267. package/src/app/health/page.tsx +5 -0
  268. package/src/app/hello/page.tsx +15 -0
  269. package/src/app/icon.png +0 -0
  270. package/src/app/layout.tsx +34 -0
  271. package/src/app/page.tsx +986 -0
  272. package/src/app/providers.tsx +90 -0
  273. package/src/app/share/[token]/page.tsx +295 -0
  274. package/src/components/ChainSelector.tsx +144 -0
  275. package/src/components/HumanActionBar.tsx +695 -0
  276. package/src/components/NotificationDrawer.tsx +129 -0
  277. package/src/components/apps/AgentKeysApp.tsx +490 -0
  278. package/src/components/apps/App.tsx +153 -0
  279. package/src/components/apps/AppGrid.tsx +15 -0
  280. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  281. package/src/components/apps/DraggableApp.tsx +562 -0
  282. package/src/components/apps/IFrameApp.tsx +73 -0
  283. package/src/components/apps/LogsApp.tsx +360 -0
  284. package/src/components/apps/SendApp.tsx +394 -0
  285. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  286. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  287. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  288. package/src/components/apps/TokenApp.tsx +319 -0
  289. package/src/components/apps/TransactionsApp.tsx +438 -0
  290. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  291. package/src/components/apps/index.ts +13 -0
  292. package/src/components/design-system/Button.tsx +53 -0
  293. package/src/components/design-system/ChainIndicator.tsx +65 -0
  294. package/src/components/design-system/ChainSelector.tsx +137 -0
  295. package/src/components/design-system/ConfirmationModal.tsx +106 -0
  296. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  297. package/src/components/design-system/Drawer.tsx +123 -0
  298. package/src/components/design-system/FilterDropdown.tsx +72 -0
  299. package/src/components/design-system/Modal.tsx +206 -0
  300. package/src/components/design-system/Popover.tsx +142 -0
  301. package/src/components/design-system/TextInput.tsx +85 -0
  302. package/src/components/design-system/Toggle.tsx +58 -0
  303. package/src/components/design-system/index.ts +11 -0
  304. package/src/components/docs/DocsThemeToggle.tsx +49 -0
  305. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  306. package/src/components/icons/ChainIcons.tsx +72 -0
  307. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  308. package/src/components/layout/ContentArea.tsx +21 -0
  309. package/src/components/layout/TabBar.tsx +278 -0
  310. package/src/components/layout/WalletSidebar.tsx +1033 -0
  311. package/src/components/layout/index.ts +4 -0
  312. package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
  313. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  314. package/src/components/vault/ApiKeysConsole.tsx +1080 -0
  315. package/src/components/vault/AuditConsole.tsx +584 -0
  316. package/src/components/vault/CredentialDetail.tsx +455 -0
  317. package/src/components/vault/CredentialEmpty.tsx +55 -0
  318. package/src/components/vault/CredentialField.tsx +361 -0
  319. package/src/components/vault/CredentialForm.tsx +1212 -0
  320. package/src/components/vault/CredentialList.tsx +165 -0
  321. package/src/components/vault/CredentialRow.tsx +97 -0
  322. package/src/components/vault/CredentialShareModal.tsx +178 -0
  323. package/src/components/vault/CredentialVault.tsx +754 -0
  324. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  325. package/src/components/vault/ImportCredentialsModal.tsx +515 -0
  326. package/src/components/vault/LargeTypeModal.tsx +64 -0
  327. package/src/components/vault/PasswordGenerator.tsx +224 -0
  328. package/src/components/vault/TOTPDisplay.tsx +123 -0
  329. package/src/components/vault/VaultSidebar.tsx +413 -0
  330. package/src/components/vault/types.ts +54 -0
  331. package/src/context/AuthContext.tsx +337 -0
  332. package/src/context/PriceContext.tsx +113 -0
  333. package/src/context/ThemeContext.tsx +164 -0
  334. package/src/context/WebSocketContext.tsx +269 -0
  335. package/src/context/WorkspaceContext.tsx +668 -0
  336. package/src/hooks/index.ts +3 -0
  337. package/src/hooks/useAgentActions.ts +368 -0
  338. package/src/hooks/useBalance.ts +103 -0
  339. package/src/hooks/useBalances.ts +129 -0
  340. package/src/instrumentation.ts +12 -0
  341. package/src/lib/api.ts +449 -0
  342. package/src/lib/app-loader.ts +148 -0
  343. package/src/lib/app-registry.ts +178 -0
  344. package/src/lib/app-sdk.ts +157 -0
  345. package/src/lib/audit-console-adapter.ts +151 -0
  346. package/src/lib/auth-client.ts +75 -0
  347. package/src/lib/config.ts +74 -0
  348. package/src/lib/crypto.ts +112 -0
  349. package/src/lib/db.ts +21 -0
  350. package/src/lib/docs.ts +390 -0
  351. package/src/lib/events.ts +361 -0
  352. package/src/lib/pino.ts +24 -0
  353. package/src/lib/theme-handlers.ts +168 -0
  354. package/src/lib/theme.ts +351 -0
  355. package/src/lib/tokenData.ts +378 -0
  356. package/src/lib/vault-crypto.ts +129 -0
  357. package/src/lib/websocket-server.ts +302 -0
  358. package/src/lib/websocket-setup.ts +79 -0
  359. package/src/lib/wordlist.ts +2050 -0
  360. package/src/lib/workspace-handlers.ts +285 -0
  361. package/start.sh +80 -0
  362. package/tailwind.config.ts +99 -0
  363. package/tsconfig.json +42 -0
@@ -0,0 +1,1033 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
4
+ import { Shield, Flame, Send, ArrowDownToLine, Lock, Unlock, Copy, ExternalLink, LockKeyhole, RefreshCw, KeyRound, Trash2, Zap, Search, X, Loader2, Plus, Eye, EyeOff, PanelLeftClose, PanelLeftOpen, Settings, Moon, Sun, BookOpen, Code2 } from 'lucide-react';
5
+ import { usePrice } from '@/context/PriceContext';
6
+ import { useAuth } from '@/context/AuthContext';
7
+ import { useWebSocket } from '@/context/WebSocketContext';
8
+ import { api, Api, unlockWallet, setupWallet } from '@/lib/api';
9
+ import { encryptPassword } from '@/lib/crypto';
10
+ import { Button, TextInput, ConfirmationModal, ChainSelector, Popover } from '@/components/design-system';
11
+ import { useTheme } from '@/context/ThemeContext';
12
+
13
+ interface Wallet {
14
+ address: string;
15
+ tier: 'cold' | 'hot' | 'temp';
16
+ chain: string;
17
+ balance?: string;
18
+ label?: string;
19
+ name?: string;
20
+ color?: string;
21
+ emoji?: string;
22
+ description?: string;
23
+ hidden?: boolean;
24
+ tokenHash?: string;
25
+ createdAt?: string;
26
+ }
27
+
28
+ interface VaultInfo {
29
+ id: string;
30
+ name?: string;
31
+ address: string;
32
+ solanaAddress?: string;
33
+ isUnlocked: boolean;
34
+ isPrimary: boolean;
35
+ createdAt: string;
36
+ }
37
+
38
+ interface WalletSidebarProps {
39
+ // Action callbacks
40
+ onSend?: () => void;
41
+ onReceive?: () => void;
42
+ onLogs?: () => void;
43
+ onAgentKeys?: () => void;
44
+ onAppStore?: () => void;
45
+ onWalletClick?: (wallet: Wallet) => void;
46
+ onImportSeed?: () => void;
47
+ onSettings?: () => void;
48
+ // External state sync (optional)
49
+ pendingActionCount?: number;
50
+ onStateChange?: (state: { configured: boolean; unlocked: boolean; wallets: Wallet[] }) => void;
51
+ }
52
+
53
+ interface AppState {
54
+ configured: boolean;
55
+ unlocked: boolean;
56
+ wallets: Wallet[];
57
+ vaults: VaultInfo[];
58
+ }
59
+
60
+ // CSS animation keyframes for wallet list
61
+ const walletAnimationStyles = `
62
+ @keyframes wallet-slide-in {
63
+ 0% {
64
+ opacity: 0;
65
+ transform: translateX(-20px) scale(0.95);
66
+ }
67
+ 100% {
68
+ opacity: 1;
69
+ transform: translateX(0) scale(1);
70
+ }
71
+ }
72
+ `;
73
+
74
+ export const WalletSidebar: React.FC<WalletSidebarProps> = ({
75
+ onSend,
76
+ onReceive,
77
+ onLogs,
78
+ onAppStore,
79
+ onWalletClick,
80
+ onImportSeed,
81
+ onSettings,
82
+ onStateChange,
83
+ }) => {
84
+ const { ethPrice, solPrice, formatUsd, formatUsdForChain } = usePrice();
85
+ const { getConfiguredChains, token, setToken } = useAuth();
86
+ const { subscribe } = useWebSocket();
87
+ const { mode, toggleMode } = useTheme();
88
+
89
+ // Chain helpers
90
+ const isSolanaChain = (chain: string) => chain === 'solana' || chain === 'solana-devnet';
91
+ const getCurrencySymbol = (chain: string) => {
92
+ if (isSolanaChain(chain)) return 'SOL';
93
+ if (chain === 'polygon') return 'MATIC';
94
+ return 'ETH';
95
+ };
96
+ const getExplorerLink = (address: string, chain: string) => {
97
+ if (isSolanaChain(chain)) return `https://solscan.io/account/${address}`;
98
+ if (chain === 'polygon') return `https://polygonscan.com/address/${address}`;
99
+ return `https://basescan.org/address/${address}`;
100
+ };
101
+
102
+ // Use ref for callback to avoid infinite loops
103
+ const onStateChangeRef = useRef(onStateChange);
104
+ onStateChangeRef.current = onStateChange;
105
+
106
+ // Internal state
107
+ const [state, setState] = useState<AppState | null>(null);
108
+ const [loading, setLoading] = useState(true);
109
+ const [selectedChain, setSelectedChain] = useState('base');
110
+ const [refreshing, setRefreshing] = useState(false);
111
+ const [copied, setCopied] = useState<string | null>(null);
112
+
113
+ // Lock/unlock state
114
+ const [unlockPassword, setUnlockPassword] = useState('');
115
+ const [unlocking, setUnlocking] = useState(false);
116
+ const [locking, setLocking] = useState(false);
117
+ // Per-vault unlock
118
+ const [vaultUnlockId, setVaultUnlockId] = useState<string | null>(null);
119
+ const [vaultUnlockPassword, setVaultUnlockPassword] = useState('');
120
+ const [vaultUnlocking, setVaultUnlocking] = useState(false);
121
+
122
+ // Setup state
123
+ const [setupPassword, setSetupPassword] = useState('');
124
+ const [setupLoading, setSetupLoading] = useState(false);
125
+
126
+ // Nuke state
127
+ const [showNukeModal, setShowNukeModal] = useState(false);
128
+ const [nuking, setNuking] = useState(false);
129
+
130
+ // Create wallet state
131
+ const [creatingWallet, setCreatingWallet] = useState(false);
132
+ const [showAddPopover, setShowAddPopover] = useState(false);
133
+ const [addPopoverView, setAddPopoverView] = useState<'menu' | 'new-vault'>('menu');
134
+ const [newVaultPassword, setNewVaultPassword] = useState('');
135
+ const [newVaultName, setNewVaultName] = useState('');
136
+ const [creatingVault, setCreatingVault] = useState(false);
137
+
138
+ // Collapse state — persist to localStorage
139
+ const [collapsed, setCollapsed] = useState(() => {
140
+ if (typeof window === 'undefined') return false;
141
+ return localStorage.getItem('sidebar-collapsed') === '1';
142
+ });
143
+ const toggleCollapsed = useCallback((val: boolean) => {
144
+ setCollapsed(val);
145
+ localStorage.setItem('sidebar-collapsed', val ? '1' : '0');
146
+ }, []);
147
+
148
+ // Search state
149
+ const [searchQuery, setSearchQuery] = useState('');
150
+ const [debouncedQuery, setDebouncedQuery] = useState('');
151
+
152
+ const chains = getConfiguredChains();
153
+ const shortAddress = (addr: string) => `${addr.slice(0, 6)}...${addr.slice(-4)}`;
154
+
155
+ // Use backend-provided balances directly (no separate RPC fetch)
156
+ const wallets = state?.wallets || [];
157
+ const vaults = state?.vaults || [];
158
+
159
+ // Find cold wallets matching the selected chain (from wallet list, not vaults)
160
+ const coldWallets = useMemo(() => {
161
+ return wallets.filter(w => w.tier === 'cold' && (
162
+ isSolanaChain(selectedChain) ? isSolanaChain(w.chain) : !isSolanaChain(w.chain)
163
+ ));
164
+ }, [wallets, selectedChain]);
165
+
166
+ // Filter hot wallets by chain family
167
+ const hotWallets = useMemo(() => {
168
+ return wallets.filter(w => w.tier === 'hot' && (
169
+ isSolanaChain(selectedChain) ? isSolanaChain(w.chain) : !isSolanaChain(w.chain)
170
+ ));
171
+ }, [wallets, selectedChain]);
172
+ // Show unlock form if wallet is configured AND (server locked OR no frontend token)
173
+ const isLocked = state?.configured && (!state?.unlocked || !token);
174
+ const isConfigured = state?.configured ?? false;
175
+
176
+ // Fetch state from server
177
+ const fetchState = useCallback(async () => {
178
+ try {
179
+ const data = await api.get<{ wallets: Wallet[]; unlocked: boolean; vaults: VaultInfo[] }>(
180
+ Api.Wallet,
181
+ '/wallets',
182
+ { includeHidden: true }
183
+ );
184
+
185
+ const configured = data.wallets.some(w => w.tier === 'cold') || (data.vaults && data.vaults.length > 0);
186
+ const newState = {
187
+ configured,
188
+ unlocked: data.unlocked,
189
+ wallets: data.wallets,
190
+ vaults: data.vaults || [],
191
+ };
192
+
193
+ setState(newState);
194
+ onStateChangeRef.current?.({
195
+ configured: newState.configured,
196
+ unlocked: newState.unlocked,
197
+ wallets: newState.wallets,
198
+ });
199
+ } catch (err) {
200
+ console.error('[WalletSidebar] Failed to fetch state:', err);
201
+ setState({ configured: false, unlocked: false, wallets: [], vaults: [] });
202
+ } finally {
203
+ setLoading(false);
204
+ setRefreshing(false);
205
+ }
206
+ }, []);
207
+
208
+ // Initial fetch
209
+ useEffect(() => {
210
+ fetchState();
211
+ }, [fetchState]);
212
+
213
+ // WebSocket subscription for wallet events (created + updated)
214
+ useEffect(() => {
215
+ const unsubscribe = subscribe('wallet:changed', () => {
216
+ fetchState();
217
+ });
218
+ return unsubscribe;
219
+ }, [subscribe, fetchState]);
220
+
221
+ // Debounce search
222
+ useEffect(() => {
223
+ const timer = setTimeout(() => setDebouncedQuery(searchQuery), 300);
224
+ return () => clearTimeout(timer);
225
+ }, [searchQuery]);
226
+
227
+ // Filter hot wallets
228
+ const filteredHotWallets = useMemo(() => {
229
+ let filtered: Wallet[];
230
+ if (!debouncedQuery.trim()) {
231
+ filtered = hotWallets.filter(w => !w.hidden);
232
+ } else {
233
+ const query = debouncedQuery.toLowerCase();
234
+ filtered = hotWallets.filter(w =>
235
+ w.address.toLowerCase().includes(query) ||
236
+ w.name?.toLowerCase().includes(query) ||
237
+ w.label?.toLowerCase().includes(query)
238
+ );
239
+ }
240
+ return [...filtered].sort((a, b) => {
241
+ const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
242
+ const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
243
+ return dateB - dateA || a.address.localeCompare(b.address);
244
+ });
245
+ }, [hotWallets, debouncedQuery]);
246
+
247
+ // Calculate total balance (only for selected chain)
248
+ const totalBalance = useMemo(() => {
249
+ let total = 0;
250
+ const chainWallets = wallets.filter(w =>
251
+ !w.hidden && (isSolanaChain(selectedChain) ? isSolanaChain(w.chain) : !isSolanaChain(w.chain))
252
+ );
253
+ chainWallets.forEach(w => {
254
+ if (w.balance) {
255
+ total += parseFloat(w.balance) || 0;
256
+ }
257
+ });
258
+ return total.toFixed(4);
259
+ }, [wallets, selectedChain]);
260
+
261
+ // Handlers
262
+ const handleRefresh = () => {
263
+ setRefreshing(true);
264
+ fetchState();
265
+ };
266
+
267
+ const handleChainChange = (chain: string) => {
268
+ setSelectedChain(chain);
269
+ };
270
+
271
+ const copyAddress = (address: string) => {
272
+ navigator.clipboard.writeText(address);
273
+ setCopied(address);
274
+ setTimeout(() => setCopied(null), 2000);
275
+ };
276
+
277
+ const handleUnlock = async (e: React.FormEvent) => {
278
+ e.preventDefault();
279
+ if (!unlockPassword) return;
280
+
281
+ setUnlocking(true);
282
+ try {
283
+ const data = await unlockWallet(unlockPassword);
284
+ if (data.token) {
285
+ setToken(data.token);
286
+ }
287
+ setUnlockPassword('');
288
+ fetchState();
289
+ } catch (err) {
290
+ console.error('[WalletSidebar] Unlock failed:', err);
291
+ } finally {
292
+ setUnlocking(false);
293
+ }
294
+ };
295
+
296
+ const handleVaultUnlock = async (e: React.FormEvent) => {
297
+ e.preventDefault();
298
+ if (!vaultUnlockId || !vaultUnlockPassword) return;
299
+
300
+ setVaultUnlocking(true);
301
+ try {
302
+ const data = await unlockWallet(vaultUnlockPassword, vaultUnlockId);
303
+ if (data.token) {
304
+ setToken(data.token);
305
+ }
306
+ setVaultUnlockId(null);
307
+ setVaultUnlockPassword('');
308
+ fetchState();
309
+ } catch (err) {
310
+ console.error('[WalletSidebar] Vault unlock failed:', err);
311
+ } finally {
312
+ setVaultUnlocking(false);
313
+ }
314
+ };
315
+
316
+ const handleVaultLock = async (vaultId: string) => {
317
+ try {
318
+ await api.post(Api.Wallet, `/lock/${vaultId}`, {});
319
+ fetchState();
320
+ } catch (err) {
321
+ console.error('[WalletSidebar] Vault lock failed:', err);
322
+ }
323
+ };
324
+
325
+ const handleLock = async () => {
326
+ setLocking(true);
327
+ try {
328
+ await api.post(Api.Wallet, '/lock', {});
329
+ fetchState();
330
+ } catch (err) {
331
+ console.error('[WalletSidebar] Lock failed:', err);
332
+ } finally {
333
+ setLocking(false);
334
+ }
335
+ };
336
+
337
+ const handleSetup = async (e: React.FormEvent) => {
338
+ e.preventDefault();
339
+ if (setupPassword.length < 8) return;
340
+
341
+ setSetupLoading(true);
342
+ try {
343
+ const result = await setupWallet(setupPassword);
344
+ if (result.token) setToken(result.token);
345
+ setSetupPassword('');
346
+ fetchState();
347
+ } catch (err) {
348
+ console.error('[WalletSidebar] Setup failed:', err);
349
+ } finally {
350
+ setSetupLoading(false);
351
+ }
352
+ };
353
+
354
+ const handleNuke = async () => {
355
+ setNuking(true);
356
+ try {
357
+ await api.post(Api.Wallet, '/nuke', {});
358
+ setShowNukeModal(false);
359
+ fetchState();
360
+ } catch (err) {
361
+ console.error('[WalletSidebar] Nuke failed:', err);
362
+ } finally {
363
+ setNuking(false);
364
+ }
365
+ };
366
+
367
+ const handleCreateHotWallet = async () => {
368
+ setCreatingWallet(true);
369
+ try {
370
+ await api.post(Api.Wallet, '/wallet/create', {
371
+ tier: 'hot',
372
+ chain: selectedChain,
373
+ });
374
+ // fetchState will be triggered by wallet:created WS event
375
+ } catch (err) {
376
+ console.error('[WalletSidebar] Create hot wallet failed:', err);
377
+ } finally {
378
+ setCreatingWallet(false);
379
+ }
380
+ };
381
+
382
+ const handleCreateVault = async (e: React.FormEvent) => {
383
+ e.preventDefault();
384
+ if (newVaultPassword.length < 8) return;
385
+
386
+ setCreatingVault(true);
387
+ try {
388
+ const { publicKey } = await api.get<{ publicKey: string }>(Api.Wallet, '/auth/connect');
389
+ const encrypted = await encryptPassword(newVaultPassword, publicKey);
390
+ await api.post(Api.Wallet, '/setup/vault', {
391
+ encrypted,
392
+ name: newVaultName || undefined,
393
+ });
394
+ setNewVaultPassword('');
395
+ setNewVaultName('');
396
+ setAddPopoverView('menu');
397
+ setShowAddPopover(false);
398
+ fetchState();
399
+ } catch (err) {
400
+ console.error('[WalletSidebar] Create vault failed:', err);
401
+ } finally {
402
+ setCreatingVault(false);
403
+ }
404
+ };
405
+
406
+ const handleToggleHidden = async (address: string, currentlyHidden: boolean) => {
407
+ try {
408
+ await api.post(Api.Wallet, '/wallet/rename', {
409
+ address,
410
+ hidden: !currentlyHidden,
411
+ });
412
+ // fetchState triggered by wallet:changed WS event
413
+ } catch (err) {
414
+ console.error('[WalletSidebar] Toggle hidden failed:', err);
415
+ }
416
+ };
417
+
418
+ // Get cold wallet balance for a vault by matching addresses
419
+ const getVaultBalance = (vault: VaultInfo): string => {
420
+ const chainAddr = isSolanaChain(selectedChain) ? vault.solanaAddress : vault.address;
421
+ if (!chainAddr) return '0';
422
+ const w = wallets.find(w => w.tier === 'cold' && w.address === chainAddr);
423
+ return w?.balance || '0';
424
+ };
425
+
426
+ // Loading state
427
+ if (loading) {
428
+ return (
429
+ <div className={`${collapsed ? 'w-[48px]' : 'w-[280px]'} h-full bg-[var(--color-surface,#f4f4f2)] border-r border-[var(--color-border,#d4d4d8)] shadow-lg flex items-center justify-center transition-all duration-200`}>
430
+ <Loader2 size={24} className="animate-spin text-[var(--color-text-muted,#6b7280)]" />
431
+ </div>
432
+ );
433
+ }
434
+
435
+ // Collapsed sidebar - thin icon bar
436
+ if (collapsed) {
437
+ return (
438
+ <div className="w-[48px] h-full bg-[var(--color-surface,#f4f4f2)] border-r border-[var(--color-border,#d4d4d8)] shadow-lg flex flex-col items-center relative transition-all duration-200">
439
+ {/* QR Noise Texture */}
440
+ <div className="absolute inset-0 opacity-[0.02] pointer-events-none bg-[radial-gradient(var(--color-text,#000)_1px,transparent_1px)] bg-[size:4px_4px]" />
441
+
442
+ {/* Logo */}
443
+ <div className="py-3 relative z-10">
444
+ <img
445
+ src="/logo.webp"
446
+ alt="AuraWallet"
447
+ className="w-8 h-8 object-contain"
448
+ />
449
+ </div>
450
+
451
+ {/* Icon buttons */}
452
+ <div className="flex flex-col items-center gap-1 relative z-10">
453
+ {isConfigured && !isLocked && (
454
+ <>
455
+ <button onClick={onSend} className="p-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors" title="Send">
456
+ <Send size={14} className="text-[var(--color-text-muted,#6b7280)]" />
457
+ </button>
458
+ <button onClick={onReceive} className="p-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors" title="Receive">
459
+ <ArrowDownToLine size={14} className="text-[var(--color-text-muted,#6b7280)]" />
460
+ </button>
461
+ </>
462
+ )}
463
+ </div>
464
+
465
+ {/* Bottom controls */}
466
+ <div className="mt-auto flex flex-col items-center gap-1 relative z-10">
467
+ <a href="/docs" className="p-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors" title="Docs">
468
+ <BookOpen size={14} className="text-[var(--color-text-muted,#6b7280)]" />
469
+ </a>
470
+ <a href="/docs?doc=EXTENSION.md" className="p-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors" title="Extension">
471
+ <ExternalLink size={14} className="text-[var(--color-text-muted,#6b7280)]" />
472
+ </a>
473
+ <a href="/api" className="p-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors" title="API">
474
+ <Code2 size={14} className="text-[var(--color-text-muted,#6b7280)]" />
475
+ </a>
476
+ <button onClick={onSettings} className="p-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors" title="Settings">
477
+ <Settings size={14} className="text-[var(--color-text-muted,#6b7280)]" />
478
+ </button>
479
+ <button onClick={toggleMode} className="p-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors" title={mode === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}>
480
+ {mode === 'light' ? (
481
+ <Moon size={14} className="text-[var(--color-text-muted,#6b7280)]" />
482
+ ) : (
483
+ <Sun size={14} className="text-[var(--color-text-muted,#a1a1aa)]" />
484
+ )}
485
+ </button>
486
+ </div>
487
+
488
+ {/* Status dot */}
489
+ <div className="pb-3 relative z-10">
490
+ {!isConfigured ? (
491
+ <div className="w-2 h-2 bg-[var(--color-text-muted,#6b7280)]" />
492
+ ) : isLocked ? (
493
+ <div className="w-2 h-2 bg-[var(--color-warning,#ff4d00)]" />
494
+ ) : (
495
+ <div className="w-2 h-2 bg-[var(--color-accent,#ccff00)] animate-pulse" />
496
+ )}
497
+ </div>
498
+
499
+ {/* Expand button */}
500
+ <button
501
+ onClick={() => toggleCollapsed(false)}
502
+ className="w-full py-2 border-t border-[var(--color-border,#d4d4d8)] hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors relative z-10 flex items-center justify-center"
503
+ title="Expand sidebar"
504
+ >
505
+ <PanelLeftOpen size={14} className="text-[var(--color-text-muted,#6b7280)]" />
506
+ </button>
507
+ </div>
508
+ );
509
+ }
510
+
511
+ return (
512
+ <div className="w-[280px] h-full bg-[var(--color-surface,#f4f4f2)] border-r border-[var(--color-border,#d4d4d8)] shadow-lg flex flex-col relative overflow-hidden font-mono">
513
+ <style>{walletAnimationStyles}</style>
514
+
515
+ {/* QR Noise Texture */}
516
+ <div className="absolute inset-0 opacity-[0.02] pointer-events-none bg-[radial-gradient(var(--color-text,#000)_1px,transparent_1px)] bg-[size:4px_4px]" />
517
+
518
+ {/* Header */}
519
+ <div className="px-4 py-3 border-b border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface-alt,#fafafa)] relative z-10">
520
+ <div className="flex items-center justify-between">
521
+ <div className="flex items-center gap-3">
522
+ <div className="w-8 h-8">
523
+ <img src="/logo.webp" alt="AuraWallet" className="w-full h-full object-contain" />
524
+ </div>
525
+ <div className="font-black text-sm tracking-tighter text-[var(--color-text,#0a0a0a)]">AURAWALLET</div>
526
+ </div>
527
+ <button
528
+ onClick={() => toggleCollapsed(true)}
529
+ className="p-1.5 hover:bg-[var(--color-surface,#ffffff)]/50 transition-colors"
530
+ title="Collapse sidebar"
531
+ >
532
+ <PanelLeftClose size={12} className="text-[var(--color-text-muted,#6b7280)]" />
533
+ </button>
534
+ </div>
535
+
536
+ {/* Chain Selector */}
537
+ {!isLocked && Object.keys(chains).length > 0 && (
538
+ <div className="mt-2 flex items-center gap-1.5">
539
+ <ChainSelector
540
+ value={selectedChain}
541
+ onChange={handleChainChange}
542
+ chains={Object.keys(chains)}
543
+ size="sm"
544
+ />
545
+ <button
546
+ onClick={handleRefresh}
547
+ disabled={refreshing}
548
+ className="p-1.5 border border-[var(--color-border,#d4d4d8)] hover:border-[var(--color-text,#0a0a0a)] bg-[var(--color-surface-alt,#f5f5f5)] hover:bg-[var(--color-background-alt,#e8e8e6)] transition-colors disabled:opacity-50"
549
+ >
550
+ <RefreshCw size={10} className={`${refreshing ? 'animate-spin' : ''} text-[var(--color-text-muted,#6b7280)]`} />
551
+ </button>
552
+ </div>
553
+ )}
554
+ </div>
555
+
556
+ {/* Total Balance / Lock Status / Setup */}
557
+ <div className="p-4 border-b border-[var(--color-border,#d4d4d8)] relative z-10">
558
+ {!isConfigured ? (
559
+ /* Setup */
560
+ <>
561
+ <div className="flex items-center gap-2 mb-3">
562
+ <div className="w-8 h-8 bg-[var(--color-accent,#ccff00)]/20 flex items-center justify-center">
563
+ <Zap size={14} className="text-[var(--color-text,#0a0a0a)]" />
564
+ </div>
565
+ <div>
566
+ <div className="font-mono text-[9px] text-[var(--color-text,#0a0a0a)] tracking-widest font-bold">INITIALIZE_VAULT</div>
567
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">Create your secure wallet</div>
568
+ </div>
569
+ </div>
570
+
571
+ <div className="mb-3 p-2 bg-[var(--color-surface-alt,#fafafa)] border border-[var(--color-border,#d4d4d8)] space-y-2">
572
+ <div className="flex items-start gap-2">
573
+ <Shield size={10} className="text-[var(--color-info,#0047ff)] mt-0.5 flex-shrink-0" />
574
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
575
+ <span className="text-[var(--color-info,#0047ff)] font-bold">COLD_VAULT</span> - Encrypted seed
576
+ </div>
577
+ </div>
578
+ <div className="flex items-start gap-2">
579
+ <Flame size={10} className="text-[var(--color-warning,#ff4d00)] mt-0.5 flex-shrink-0" />
580
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
581
+ <span className="text-[var(--color-warning,#ff4d00)] font-bold">HOT_WALLETS</span> - Derived keys
582
+ </div>
583
+ </div>
584
+ </div>
585
+
586
+ <form onSubmit={handleSetup} className="space-y-2">
587
+ <TextInput
588
+ label="ENCRYPTION_PASSWORD"
589
+ type="password"
590
+ value={setupPassword}
591
+ onChange={(e) => setSetupPassword(e.target.value)}
592
+ placeholder="Min 8 characters"
593
+ minLength={8}
594
+ compact
595
+ />
596
+ <Button
597
+ type="submit"
598
+ disabled={setupLoading || setupPassword.length < 8}
599
+ loading={setupLoading}
600
+ icon={!setupLoading ? <Zap size={12} /> : undefined}
601
+ className="w-full"
602
+ size="lg"
603
+ >
604
+ {setupLoading ? 'INITIALIZING...' : 'INITIALIZE'}
605
+ </Button>
606
+ </form>
607
+
608
+ <div className="mt-3 pt-3 border-t border-[var(--color-border,#d4d4d8)]">
609
+ <Button
610
+ variant="secondary"
611
+ size="md"
612
+ onClick={onImportSeed}
613
+ icon={<KeyRound size={10} />}
614
+ className="w-full"
615
+ >
616
+ IMPORT_EXISTING_SEED
617
+ </Button>
618
+ </div>
619
+ </>
620
+ ) : isLocked ? (
621
+ /* Locked */
622
+ <>
623
+ <div className="flex items-center gap-2 mb-2">
624
+ <div className="w-8 h-8 bg-[var(--color-warning,#ff4d00)]/10 flex items-center justify-center">
625
+ <LockKeyhole size={14} className="text-[var(--color-warning,#ff4d00)]" />
626
+ </div>
627
+ <div>
628
+ <div className="font-mono text-[9px] text-[var(--color-warning,#ff4d00)] tracking-widest font-bold">{state?.unlocked ? 'AUTHENTICATE' : 'VAULT_LOCKED'}</div>
629
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">{state?.unlocked ? 'Enter password to connect' : 'Enter password to unlock'}</div>
630
+ </div>
631
+ </div>
632
+ <form onSubmit={handleUnlock} className="space-y-2">
633
+ <TextInput
634
+ label="PASSWORD"
635
+ type="password"
636
+ value={unlockPassword}
637
+ onChange={(e) => setUnlockPassword(e.target.value)}
638
+ placeholder="Enter password"
639
+ compact
640
+ />
641
+ <Button
642
+ type="submit"
643
+ disabled={unlocking || !unlockPassword}
644
+ loading={unlocking}
645
+ icon={!unlocking ? <Unlock size={12} /> : undefined}
646
+ className="w-full"
647
+ size="lg"
648
+ >
649
+ {unlocking ? 'UNLOCKING...' : 'UNLOCK'}
650
+ </Button>
651
+ </form>
652
+
653
+ </>
654
+ ) : (
655
+ /* Unlocked */
656
+ <>
657
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)] tracking-widest mb-1">TOTAL_BALANCE</div>
658
+ <div className="font-black text-2xl text-[var(--color-text,#0a0a0a)] tracking-tight">{totalBalance} {getCurrencySymbol(selectedChain)}</div>
659
+ {formatUsdForChain(totalBalance, selectedChain) && (
660
+ <div className="font-mono text-sm text-[var(--color-text-muted,#6b7280)] mt-0.5">{formatUsdForChain(totalBalance, selectedChain)}</div>
661
+ )}
662
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)] mt-2 flex items-center justify-between">
663
+ <div className="flex items-center gap-1">
664
+ <div className="w-1 h-1 bg-[var(--color-accent,#ccff00)]" />
665
+ {selectedChain.toUpperCase()}
666
+ </div>
667
+ {isSolanaChain(selectedChain) ? (
668
+ solPrice && <span className="text-[var(--color-info,#0047ff)]">SOL ${solPrice.toLocaleString()}</span>
669
+ ) : (
670
+ ethPrice && <span className="text-[var(--color-info,#0047ff)]">ETH ${ethPrice.toLocaleString()}</span>
671
+ )}
672
+ </div>
673
+ </>
674
+ )}
675
+ </div>
676
+
677
+ {/* Primary Actions */}
678
+ {isConfigured && !isLocked && (
679
+ <div className="px-3 py-2 border-b border-[var(--color-border,#d4d4d8)] relative z-10">
680
+ <div className="flex gap-2">
681
+ <Button variant="secondary" size="md" onClick={onSend} icon={<Send size={10} />} className="flex-1">
682
+ SEND
683
+ </Button>
684
+ <Button variant="secondary" size="md" onClick={onReceive} icon={<ArrowDownToLine size={10} />} className="flex-1">
685
+ RECEIVE
686
+ </Button>
687
+ </div>
688
+ </div>
689
+ )}
690
+
691
+ {/* Wallet List */}
692
+ <div className="flex-1 overflow-y-auto relative z-10">
693
+ {!isConfigured ? (
694
+ <div className="p-4 flex items-center justify-center h-full">
695
+ <div className="text-center opacity-50">
696
+ <Zap size={32} className="mx-auto mb-2 text-[var(--color-text-muted,#6b7280)]" />
697
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">AWAITING_INIT</div>
698
+ </div>
699
+ </div>
700
+ ) : isLocked ? (
701
+ <div className="p-4 flex items-center justify-center h-full">
702
+ <div className="text-center opacity-30">
703
+ <Lock size={32} className="mx-auto mb-2 text-[var(--color-text-muted,#6b7280)]" />
704
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">ASSETS_HIDDEN</div>
705
+ </div>
706
+ </div>
707
+ ) : (
708
+ <div className="p-4">
709
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)] tracking-widest mb-3">ASSETS</div>
710
+
711
+ {/* Search + Add */}
712
+ <div className="mb-3 flex gap-1.5">
713
+ <div className="relative flex-1">
714
+ <div className="absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none">
715
+ <Search size={10} className="text-[var(--color-text-muted,#6b7280)]" />
716
+ </div>
717
+ <input
718
+ type="text"
719
+ value={searchQuery}
720
+ onChange={(e) => setSearchQuery(e.target.value)}
721
+ placeholder="Search wallets..."
722
+ className="w-full pl-7 pr-7 py-1.5 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px] text-[var(--color-text,#0a0a0a)] focus:outline-none focus:border-[var(--color-border-focus,#0a0a0a)] bg-[var(--color-surface,#ffffff)] placeholder-[var(--color-text-faint,#9ca3af)]"
723
+ />
724
+ {searchQuery && (
725
+ <button
726
+ onClick={() => setSearchQuery('')}
727
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-[var(--color-surface-alt,#f5f5f5)] rounded"
728
+ >
729
+ <X size={10} className="text-[var(--color-text-muted,#6b7280)]" />
730
+ </button>
731
+ )}
732
+ </div>
733
+ <div className="relative">
734
+ <button
735
+ onClick={() => setShowAddPopover(!showAddPopover)}
736
+ className="h-full px-2 border border-[var(--color-border,#d4d4d8)] hover:border-[var(--color-text,#0a0a0a)] bg-[var(--color-surface-alt,#f5f5f5)] hover:bg-[var(--color-background-alt,#e8e8e6)] transition-colors"
737
+ title="Add wallet"
738
+ >
739
+ <Plus size={10} className="text-[var(--color-text-muted,#6b7280)]" />
740
+ </button>
741
+ <Popover isOpen={showAddPopover} onClose={() => { setShowAddPopover(false); setAddPopoverView('menu'); setNewVaultPassword(''); setNewVaultName(''); }} title={addPopoverView === 'menu' ? 'ADD_WALLET' : 'NEW_VAULT'} anchor="right">
742
+ {addPopoverView === 'menu' ? (
743
+ <div className="space-y-1 min-w-[160px]">
744
+ <button
745
+ onClick={() => setAddPopoverView('new-vault')}
746
+ className="w-full flex items-center gap-2 px-2 py-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors text-left"
747
+ >
748
+ <Shield size={10} className="text-[var(--color-info,#0047ff)]" />
749
+ <div>
750
+ <div className="font-mono text-[9px] font-bold text-[var(--color-text,#0a0a0a)]">NEW_VAULT</div>
751
+ <div className="font-mono text-[7px] text-[var(--color-text-muted,#6b7280)]">Generate new seed phrase</div>
752
+ </div>
753
+ </button>
754
+ <button
755
+ onClick={() => { setShowAddPopover(false); setAddPopoverView('menu'); onImportSeed?.(); }}
756
+ className="w-full flex items-center gap-2 px-2 py-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors text-left"
757
+ >
758
+ <KeyRound size={10} className="text-[var(--color-info,#0047ff)]" />
759
+ <div>
760
+ <div className="font-mono text-[9px] font-bold text-[var(--color-text,#0a0a0a)]">IMPORT_VAULT</div>
761
+ <div className="font-mono text-[7px] text-[var(--color-text-muted,#6b7280)]">From existing seed phrase</div>
762
+ </div>
763
+ </button>
764
+ <button
765
+ onClick={() => { setShowAddPopover(false); handleCreateHotWallet(); }}
766
+ disabled={creatingWallet}
767
+ className="w-full flex items-center gap-2 px-2 py-2 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors text-left disabled:opacity-50"
768
+ >
769
+ <Flame size={10} className="text-[var(--color-warning,#ff4d00)]" />
770
+ <div>
771
+ <div className="font-mono text-[9px] font-bold text-[var(--color-text,#0a0a0a)]">ADD_HOT_WALLET</div>
772
+ <div className="font-mono text-[7px] text-[var(--color-text-muted,#6b7280)]">New agent-accessible wallet</div>
773
+ </div>
774
+ </button>
775
+ </div>
776
+ ) : (
777
+ <form onSubmit={handleCreateVault} className="space-y-2 min-w-[200px]">
778
+ <input
779
+ type="text"
780
+ value={newVaultName}
781
+ onChange={(e) => setNewVaultName(e.target.value)}
782
+ placeholder="Vault name (optional)"
783
+ className="w-full px-2 py-1.5 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px] text-[var(--color-text,#0a0a0a)] focus:outline-none focus:border-[var(--color-info,#0047ff)] bg-[var(--color-surface,#ffffff)] placeholder-[var(--color-text-faint,#9ca3af)]"
784
+ />
785
+ <input
786
+ type="password"
787
+ value={newVaultPassword}
788
+ onChange={(e) => setNewVaultPassword(e.target.value)}
789
+ placeholder="Password (min 8 chars)"
790
+ className="w-full px-2 py-1.5 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px] text-[var(--color-text,#0a0a0a)] focus:outline-none focus:border-[var(--color-info,#0047ff)] bg-[var(--color-surface,#ffffff)] placeholder-[var(--color-text-faint,#9ca3af)]"
791
+ />
792
+ <div className="flex gap-1">
793
+ <Button type="submit" size="sm" disabled={creatingVault || newVaultPassword.length < 8} loading={creatingVault} className="flex-1">
794
+ CREATE
795
+ </Button>
796
+ <Button variant="ghost" size="sm" onClick={() => { setAddPopoverView('menu'); setNewVaultPassword(''); setNewVaultName(''); }}>
797
+ <X size={9} />
798
+ </Button>
799
+ </div>
800
+ </form>
801
+ )}
802
+ </Popover>
803
+ </div>
804
+ </div>
805
+
806
+ {/* Vaults */}
807
+ {vaults.map((vault) => {
808
+ const balance = getVaultBalance(vault);
809
+ const vaultAddr = isSolanaChain(selectedChain) ? vault.solanaAddress : vault.address;
810
+ if (!vaultAddr) return null;
811
+
812
+ return (
813
+ <div key={vault.id} className="mb-3 border-2 border-[var(--color-info,#0047ff)] bg-gradient-to-r from-[var(--color-info,#0047ff)]/10 to-transparent p-3 relative">
814
+ <div className="absolute top-0 left-0 w-full h-0.5 bg-[var(--color-info,#0047ff)]" />
815
+ {vault.isUnlocked && (
816
+ <div className="absolute top-1 right-1 w-1.5 h-1.5 bg-[var(--color-info,#0047ff)] animate-pulse" />
817
+ )}
818
+ {!vault.isUnlocked && (
819
+ <div className="absolute top-1 right-1 w-1.5 h-1.5 bg-[var(--color-warning,#ff4d00)]" />
820
+ )}
821
+ <div className="flex items-center gap-2 mb-2">
822
+ <Shield size={12} className="text-[var(--color-info,#0047ff)]" />
823
+ <span className="font-mono text-[9px] text-[var(--color-info,#0047ff)] tracking-widest font-bold truncate flex-1">
824
+ {vault.name || (vault.isPrimary ? 'PRIMARY_VAULT' : `VAULT_${vault.id.toUpperCase()}`)}
825
+ </span>
826
+ {vault.isUnlocked ? (
827
+ <button
828
+ onClick={() => handleVaultLock(vault.id)}
829
+ className="p-1 hover:bg-[var(--color-info,#0047ff)]/10 transition-colors"
830
+ title="Lock vault"
831
+ >
832
+ <Lock size={9} className="text-[var(--color-text-muted,#6b7280)]" />
833
+ </button>
834
+ ) : (
835
+ <button
836
+ onClick={() => setVaultUnlockId(vault.id)}
837
+ className="p-1 hover:bg-[var(--color-info,#0047ff)]/10 transition-colors"
838
+ title="Unlock vault"
839
+ >
840
+ <Unlock size={9} className="text-[var(--color-warning,#ff4d00)]" />
841
+ </button>
842
+ )}
843
+ </div>
844
+
845
+ {/* Per-vault unlock form */}
846
+ {vaultUnlockId === vault.id && !vault.isUnlocked && (
847
+ <form onSubmit={handleVaultUnlock} className="mb-2 space-y-1.5">
848
+ <input
849
+ type="password"
850
+ value={vaultUnlockPassword}
851
+ onChange={(e) => setVaultUnlockPassword(e.target.value)}
852
+ placeholder="Vault password"
853
+ className="w-full px-2 py-1 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px] text-[var(--color-text,#0a0a0a)] focus:outline-none focus:border-[var(--color-info,#0047ff)] bg-[var(--color-surface,#ffffff)]"
854
+ />
855
+ <div className="flex gap-1">
856
+ <Button type="submit" size="sm" disabled={vaultUnlocking || !vaultUnlockPassword} loading={vaultUnlocking} className="flex-1">
857
+ UNLOCK
858
+ </Button>
859
+ <Button variant="ghost" size="sm" onClick={() => { setVaultUnlockId(null); setVaultUnlockPassword(''); }}>
860
+ <X size={9} />
861
+ </Button>
862
+ </div>
863
+ </form>
864
+ )}
865
+
866
+ <div className="flex items-center justify-between">
867
+ <div>
868
+ <div className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">{shortAddress(vaultAddr)}</div>
869
+ <div className="font-bold text-sm text-[var(--color-text,#0a0a0a)] mt-0.5">{balance} {getCurrencySymbol(selectedChain)}</div>
870
+ {formatUsdForChain(balance, selectedChain) && (
871
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">{formatUsdForChain(balance, selectedChain)}</div>
872
+ )}
873
+ </div>
874
+ <div className="flex gap-1">
875
+ {!vault.isUnlocked && (
876
+ <span className="font-mono text-[7px] text-[var(--color-warning,#ff4d00)] bg-[var(--color-warning,#ff4d00)]/10 px-1 py-0.5 self-center">LOCKED</span>
877
+ )}
878
+ <button onClick={() => copyAddress(vaultAddr)} className="p-1.5 hover:bg-[var(--color-info,#0047ff)]/10 transition-colors">
879
+ <Copy size={10} className={copied === vaultAddr ? 'text-[var(--color-accent,#ccff00)]' : 'text-[var(--color-text-muted,#6b7280)]'} />
880
+ </button>
881
+ <a href={getExplorerLink(vaultAddr, selectedChain)} target="_blank" className="p-1.5 hover:bg-[var(--color-info,#0047ff)]/10 transition-colors">
882
+ <ExternalLink size={10} className="text-[var(--color-text-muted,#6b7280)]" />
883
+ </a>
884
+ </div>
885
+ </div>
886
+ </div>
887
+ );
888
+ })}
889
+
890
+ {/* Hot Wallets */}
891
+ <div className="space-y-1">
892
+ {filteredHotWallets.map((wallet) => {
893
+ const walletColor = wallet.color || '#ff4d00';
894
+ return (
895
+ <div
896
+ key={wallet.address}
897
+ onClick={() => onWalletClick?.(wallet)}
898
+ className={`bg-[var(--color-surface,#ffffff)] p-2.5 relative group hover:border-[var(--color-warning,#ff4d00)] cursor-pointer ${wallet.hidden ? 'opacity-60' : ''}`}
899
+ style={{
900
+ borderWidth: '1px',
901
+ borderColor: `${walletColor}4D`,
902
+ borderTopWidth: wallet.color ? '3px' : '1px',
903
+ borderTopColor: wallet.color || `${walletColor}4D`,
904
+ transition: 'transform 0.3s ease-out, opacity 0.3s ease-out, border-color 0.15s ease',
905
+ animation: wallet.createdAt && (Date.now() - new Date(wallet.createdAt).getTime() < 2000)
906
+ ? 'wallet-slide-in 0.3s ease-out'
907
+ : undefined,
908
+ }}
909
+ >
910
+ <div className="absolute top-1 right-1 w-1.5 h-1.5" style={{ backgroundColor: walletColor, opacity: 0.7 }} />
911
+ <div className="flex items-center gap-2">
912
+ {wallet.emoji && <span className="text-[10px]">{wallet.emoji}</span>}
913
+ <span className="font-mono text-[9px] font-bold truncate flex-1" style={{ color: walletColor }}>
914
+ {wallet.name || wallet.label || 'HOT'}
915
+ </span>
916
+ {wallet.hidden && (
917
+ <span className="font-mono text-[7px] text-[var(--color-text-muted,#6b7280)] bg-[var(--color-surface-alt,#f5f5f5)] px-1">HIDDEN</span>
918
+ )}
919
+ <button
920
+ onClick={(e) => { e.stopPropagation(); handleToggleHidden(wallet.address, !!wallet.hidden); }}
921
+ className="p-1 hover:bg-[var(--color-warning,#ff4d00)]/10 transition-colors opacity-0 group-hover:opacity-100"
922
+ title={wallet.hidden ? 'Show wallet' : 'Hide wallet'}
923
+ >
924
+ {wallet.hidden
925
+ ? <Eye size={9} className="text-[var(--color-text-muted,#6b7280)]" />
926
+ : <EyeOff size={9} className="text-[var(--color-text-muted,#6b7280)]" />
927
+ }
928
+ </button>
929
+ <button
930
+ onClick={(e) => { e.stopPropagation(); copyAddress(wallet.address); }}
931
+ className="p-1 hover:bg-[var(--color-warning,#ff4d00)]/10 transition-colors opacity-0 group-hover:opacity-100"
932
+ >
933
+ <Copy size={9} className={copied === wallet.address ? 'text-[var(--color-accent,#ccff00)]' : 'text-[var(--color-text-muted,#6b7280)]'} />
934
+ </button>
935
+ </div>
936
+ <div className="flex items-center justify-between mt-1">
937
+ <span className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">{shortAddress(wallet.address)}</span>
938
+ <div className="text-right">
939
+ <span className="font-mono text-[10px] font-bold text-[var(--color-text,#0a0a0a)]">{wallet.balance || '0'} {getCurrencySymbol(wallet.chain)}</span>
940
+ {formatUsdForChain(wallet.balance, wallet.chain) && (
941
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">{formatUsdForChain(wallet.balance, wallet.chain)}</div>
942
+ )}
943
+ </div>
944
+ </div>
945
+ </div>
946
+ );
947
+ })}
948
+ </div>
949
+ </div>
950
+ )}
951
+ </div>
952
+
953
+ {/* Footer */}
954
+ <div className="p-3 border-t border-[var(--color-border,#d4d4d8)] relative z-10">
955
+ <div className="flex items-center justify-between">
956
+ {isConfigured && !isLocked ? (
957
+ <Button variant="ghost" size="sm" onClick={handleLock} disabled={locking} loading={locking} icon={!locking ? <Lock size={10} /> : undefined}>
958
+ LOCK ALL
959
+ </Button>
960
+ ) : (
961
+ <div className="flex items-center gap-1.5">
962
+ {!isConfigured ? (
963
+ <>
964
+ <div className="w-1.5 h-1.5 bg-[var(--color-text-muted,#6b7280)]" />
965
+ <span className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">UNINITIALIZED</span>
966
+ </>
967
+ ) : (
968
+ <>
969
+ <div className="w-1.5 h-1.5 bg-[var(--color-warning,#ff4d00)]" />
970
+ <span className="font-mono text-[8px] text-[var(--color-warning,#ff4d00)]">{state?.unlocked ? 'NO_SESSION' : 'LOCKED'}</span>
971
+ </>
972
+ )}
973
+ </div>
974
+ )}
975
+ <div className="flex items-center gap-0.5">
976
+ <a href="/docs" className="px-1.5 py-1.5 font-mono text-[8px] tracking-widest text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">DOCS</a>
977
+ <a href="/docs?doc=EXTENSION.md" className="px-1.5 py-1.5 font-mono text-[8px] tracking-widest text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">EXTENSION</a>
978
+ <a href="/api" className="px-1.5 py-1.5 font-mono text-[8px] tracking-widest text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">API</a>
979
+ <button
980
+ onClick={onSettings}
981
+ className="p-1.5 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors"
982
+ title="Settings"
983
+ >
984
+ <Settings size={12} className="text-[var(--color-text-muted,#6b7280)]" />
985
+ </button>
986
+ <button
987
+ onClick={toggleMode}
988
+ className="p-1.5 hover:bg-[var(--color-surface-alt,#f5f5f5)] transition-colors"
989
+ title={mode === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
990
+ >
991
+ {mode === 'light' ? (
992
+ <Moon size={12} className="text-[var(--color-text-muted,#6b7280)]" />
993
+ ) : (
994
+ <Sun size={12} className="text-[var(--color-text-muted,#a1a1aa)]" />
995
+ )}
996
+ </button>
997
+ </div>
998
+ </div>
999
+ {isConfigured && !isLocked && (
1000
+ <div className="flex items-center gap-1.5 mt-2">
1001
+ <div className="w-1.5 h-1.5 bg-[var(--color-accent,#ccff00)] animate-pulse" />
1002
+ <span className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">
1003
+ {vaults.length > 1
1004
+ ? `${vaults.filter(v => v.isUnlocked).length}/${vaults.length} VAULTS`
1005
+ : 'OPERATIONAL'}
1006
+ </span>
1007
+ </div>
1008
+ )}
1009
+ </div>
1010
+
1011
+ {/* Barcode + Stripe */}
1012
+ <div className="flex items-center gap-3 px-4 py-2 border-t border-[var(--color-border,#d4d4d8)] relative z-10">
1013
+ <div className="h-4 flex-1 bg-[repeating-linear-gradient(90deg,var(--color-text,#000),var(--color-text,#000)_1px,transparent_1px,transparent_3px)] opacity-30" />
1014
+ </div>
1015
+ <div className="h-2 w-full relative z-10" style={{
1016
+ backgroundImage: 'repeating-linear-gradient(45deg, var(--color-text, #000), var(--color-text, #000) 5px, transparent 5px, transparent 10px)',
1017
+ opacity: 0.1,
1018
+ }} />
1019
+
1020
+ {/* Nuke Modal */}
1021
+ <ConfirmationModal
1022
+ isOpen={showNukeModal}
1023
+ onClose={() => setShowNukeModal(false)}
1024
+ onConfirm={handleNuke}
1025
+ title="Nuke Everything"
1026
+ message="This will permanently delete your cold wallet, all hot wallets, credentials, and configuration. Make sure you have backed up your seed phrase before proceeding."
1027
+ confirmText="NUKE"
1028
+ variant="danger"
1029
+ loading={nuking}
1030
+ />
1031
+ </div>
1032
+ );
1033
+ };