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,2122 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState, Suspense } from 'react';
4
+
5
+ import { Shield, Flame, Plus, Send, Rocket, Copy, Loader2, Check, X, AlertTriangle, Trash2, Home as HomeIcon, KeyRound, Code, Database, RotateCcw, Lock, ChevronDown, Settings, Bot } from 'lucide-react';
6
+ import { TextInput, FilterDropdown, Drawer, Modal, ConfirmationPopover, Button, Popover } from '@/components/design-system';
7
+ import { WalletSidebar, TabBar, WorkspaceTab, AppStoreDrawer } from '@/components/layout';
8
+ import { DraggableApp, type AppColor } from '@/components/apps';
9
+ import { useAgentActions } from '@/hooks/useAgentActions';
10
+ import { HumanActionBar } from '@/components/HumanActionBar';
11
+ import { useWorkspace, type AppState as WorkspaceAppState } from '@/context/WorkspaceContext';
12
+ import { useAuth, type ApiKey, type ChainConfig } from '@/context/AuthContext';
13
+ import { useWebSocket } from '@/context/WebSocketContext';
14
+ import { getAppDefinition } from '@/lib/app-registry';
15
+ import SystemDefaults, { AiEngineSection } from '@/components/apps/SystemDefaultsApp';
16
+ import { WALLET_EVENTS, WalletCreatedData } from '@/lib/events';
17
+ import { api, Api } from '@/lib/api';
18
+
19
+ // Known chains with Alchemy support - used for auto-fill when adding chains
20
+ const KNOWN_CHAINS: Record<string, { chainId: number; alchemyPath: string; explorer: string }> = {
21
+ base: { chainId: 8453, alchemyPath: 'base-mainnet', explorer: 'https://basescan.org' },
22
+ ethereum: { chainId: 1, alchemyPath: 'eth-mainnet', explorer: 'https://etherscan.io' },
23
+ arbitrum: { chainId: 42161, alchemyPath: 'arb-mainnet', explorer: 'https://arbiscan.io' },
24
+ optimism: { chainId: 10, alchemyPath: 'opt-mainnet', explorer: 'https://optimistic.etherscan.io' },
25
+ polygon: { chainId: 137, alchemyPath: 'polygon-mainnet', explorer: 'https://polygonscan.com' },
26
+ zksync: { chainId: 324, alchemyPath: 'zksync-mainnet', explorer: 'https://explorer.zksync.io' },
27
+ };
28
+
29
+ interface WalletData {
30
+ address: string;
31
+ tier: 'cold' | 'hot' | 'temp';
32
+ chain: string;
33
+ balance?: string;
34
+ label?: string;
35
+ spentToday?: number;
36
+ name?: string;
37
+ color?: string;
38
+ emoji?: string;
39
+ description?: string;
40
+ hidden?: boolean;
41
+ tokenHash?: string;
42
+ createdAt?: string;
43
+ }
44
+
45
+ interface DashboardState {
46
+ configured: boolean;
47
+ isUnlocked: boolean;
48
+ wallets: WalletData[];
49
+ }
50
+
51
+ interface AppPosition {
52
+ x: number;
53
+ y: number;
54
+ }
55
+
56
+ export default function Home() {
57
+ const {
58
+ token,
59
+ apiKeys: authApiKeys,
60
+ apiKeysLoading: authApiKeysLoading,
61
+ refreshApiKeys,
62
+ getApiKey,
63
+ chainOverrides,
64
+ saveChainOverride,
65
+ removeChainOverride,
66
+ getConfiguredChains,
67
+ } = useAuth();
68
+ const { subscribe } = useWebSocket();
69
+ const [state, setState] = useState<DashboardState | null>(null);
70
+ const [loading, setLoading] = useState(true);
71
+ const [error, setError] = useState('');
72
+
73
+ // Agent requests - only need count for sidebar badge (app is self-contained)
74
+ // Only auto-fetch when we have a token (wallet is unlocked)
75
+ const { requests, notifications, dismissNotification, resolveAction, actionLoading } = useAgentActions({ autoFetch: !!token });
76
+ const [copied, setCopied] = useState<string | null>(null);
77
+ const [activeDrawer, setActiveDrawer] = useState<'settings' | 'receive' | null>(null);
78
+ const [showAppStore, setShowAppStore] = useState(false);
79
+ const [nuking, setNuking] = useState(false);
80
+ const [confirmNuke, setConfirmNuke] = useState(false);
81
+
82
+ const [sendFrom, setSendFrom] = useState('');
83
+
84
+ const [seedPhrase, setSeedPhrase] = useState<string | null>(null);
85
+ const [seedConfirmed, setSeedConfirmed] = useState(false);
86
+ const [exportPassword, setExportPassword] = useState('');
87
+ const [exporting, setExporting] = useState(false);
88
+ const [exportedSeed, setExportedSeed] = useState<string | null>(null);
89
+
90
+ // Import seed state
91
+ const [showImportSeedModal, setShowImportSeedModal] = useState(false);
92
+ const [importSeedPhrase, setImportSeedPhrase] = useState('');
93
+ const [importPassword, setImportPassword] = useState('');
94
+ const [importConfirmPassword, setImportConfirmPassword] = useState('');
95
+ const [importing, setImporting] = useState(false);
96
+
97
+ // Chain state
98
+ const [chains, setChains] = useState<Record<string, { rpc: string; chainId: number; explorer: string; nativeCurrency: string }>>({});
99
+ const [editingChainRpc, setEditingChainRpc] = useState<string | null>(null);
100
+ const [customRpc, setCustomRpc] = useState('');
101
+ const [savingConfig] = useState(false);
102
+ const [showAddChainModal, setShowAddChainModal] = useState(false);
103
+ const [addChainAnchorEl, setAddChainAnchorEl] = useState<HTMLElement | null>(null);
104
+ const [newChain, setNewChain] = useState({ name: '', chainId: '', rpc: '', explorer: '', nativeCurrency: 'ETH' });
105
+
106
+ // API Keys state
107
+ const [showAddApiKeyPopover, setShowAddApiKeyPopover] = useState(false);
108
+ const [addApiKeyAnchorEl, setAddApiKeyAnchorEl] = useState<HTMLElement | null>(null);
109
+ const [newApiKey, setNewApiKey] = useState({ service: '', name: '', key: '' });
110
+ const [savingApiKey, setSavingApiKey] = useState(false);
111
+ const [deletingApiKey, setDeletingApiKey] = useState<string | null>(null);
112
+
113
+ // Backup state
114
+ interface BackupInfo {
115
+ filename: string;
116
+ timestamp: string;
117
+ size: number;
118
+ date: string;
119
+ }
120
+ const [backups, setBackups] = useState<BackupInfo[]>([]);
121
+ const [backupsLoading, setBackupsLoading] = useState(false);
122
+ const [creatingBackup, setCreatingBackup] = useState(false);
123
+ const [restoringBackup, setRestoringBackup] = useState<string | null>(null);
124
+
125
+ // Workspace context for programmatic workspace control
126
+ const {
127
+ workspaces,
128
+ activeWorkspaceId,
129
+ apps: workspaceApps,
130
+ loading: workspaceLoading,
131
+ createWorkspace,
132
+ deleteWorkspace,
133
+ updateWorkspace,
134
+ switchWorkspace,
135
+ addApp,
136
+ removeApp,
137
+ updateApp,
138
+ bringToFront,
139
+ tidyApps,
140
+ } = useWorkspace();
141
+
142
+ // Convert workspaces to tabs format
143
+ const tabs: WorkspaceTab[] = workspaces.map(ws => ({
144
+ id: ws.id,
145
+ label: ws.name,
146
+ icon: ws.icon === 'Home' ? HomeIcon : undefined,
147
+ emoji: ws.emoji,
148
+ color: ws.color,
149
+ closeable: ws.isCloseable,
150
+ isDefault: ws.isDefault,
151
+ }));
152
+
153
+ // Handle workspace tab update (name, emoji, color)
154
+ const handleTabUpdate = (tabId: string, data: { name?: string; emoji?: string; color?: string }) => {
155
+ updateWorkspace(tabId, {
156
+ name: data.name,
157
+ emoji: data.emoji,
158
+ color: data.color,
159
+ });
160
+ };
161
+
162
+ const fetchState = async (retries = 10): Promise<void> => {
163
+ for (let attempt = 0; attempt < retries; attempt++) {
164
+ try {
165
+ const controller = new AbortController();
166
+ const timeout = setTimeout(() => controller.abort(), 5000);
167
+
168
+ const setupData = await api.get<{ hasWallet: boolean; unlocked: boolean; address: string | null }>(
169
+ Api.Wallet, '/setup', undefined, { signal: controller.signal },
170
+ );
171
+ clearTimeout(timeout);
172
+
173
+ // Map new server response format
174
+ const configured = setupData.hasWallet;
175
+ const isUnlocked = setupData.unlocked;
176
+
177
+ if (!configured) {
178
+ setState({ configured: false, isUnlocked: false, wallets: [] });
179
+ setLoading(false);
180
+ return;
181
+ }
182
+
183
+ if (!isUnlocked) {
184
+ setState({ configured: true, isUnlocked: false, wallets: [] });
185
+ setLoading(false);
186
+ return;
187
+ }
188
+
189
+ const walletsData = await api.get<{ wallets: WalletData[] }>(Api.Wallet, '/wallets', { includeHidden: true });
190
+
191
+ setState({
192
+ configured: true,
193
+ isUnlocked: true,
194
+ wallets: walletsData.wallets || [],
195
+ });
196
+ if (walletsData.wallets?.length > 0 && !sendFrom) {
197
+ const hotWallet = walletsData.wallets.find((w: WalletData) => w.tier === 'hot');
198
+ if (hotWallet) setSendFrom(hotWallet.address);
199
+ }
200
+ setLoading(false);
201
+ return;
202
+ } catch {
203
+ // Server not ready yet — retry after a delay
204
+ if (attempt < retries - 1) {
205
+ await new Promise(r => setTimeout(r, 2000));
206
+ }
207
+ }
208
+ }
209
+ // All retries exhausted
210
+ setError('Failed to connect to wallet server');
211
+ setLoading(false);
212
+ };
213
+
214
+ useEffect(() => { fetchState(); }, []);
215
+
216
+ // Sync chains state from AuthContext (overrides + Alchemy + public fallbacks)
217
+ useEffect(() => {
218
+ const configuredChains = getConfiguredChains();
219
+ const chainsWithNative: Record<string, { rpc: string; chainId: number; explorer: string; nativeCurrency: string }> = {};
220
+ for (const [chain, config] of Object.entries(configuredChains)) {
221
+ chainsWithNative[chain] = {
222
+ ...config,
223
+ nativeCurrency: 'ETH', // All supported chains use ETH
224
+ };
225
+ }
226
+ setChains(chainsWithNative);
227
+ }, [getConfiguredChains, chainOverrides, authApiKeys]);
228
+
229
+ // Subscribe to WebSocket wallet events for real-time updates
230
+ useEffect(() => {
231
+ // Handle wallet:created - add new wallet to state
232
+ const unsubscribeWalletCreated = subscribe(WALLET_EVENTS.WALLET_CREATED, (event) => {
233
+ const data = event.data as WalletCreatedData;
234
+ setState((prev) => {
235
+ if (!prev) return prev;
236
+ // Check if wallet already exists
237
+ if (prev.wallets.some((w) => w.address === data.address)) {
238
+ return prev;
239
+ }
240
+ // Add new wallet with createdAt timestamp
241
+ const newWallet: WalletData = {
242
+ address: data.address,
243
+ tier: data.tier,
244
+ chain: data.chain,
245
+ name: data.name,
246
+ tokenHash: data.tokenHash,
247
+ balance: '0 ETH',
248
+ createdAt: new Date().toISOString(),
249
+ };
250
+ return {
251
+ ...prev,
252
+ wallets: [...prev.wallets, newWallet],
253
+ };
254
+ });
255
+ });
256
+
257
+ return () => {
258
+ unsubscribeWalletCreated();
259
+ };
260
+ }, [subscribe]);
261
+
262
+ // Seed default apps on first setup (empty workspace + configured + unlocked)
263
+ // Key is tied to cold wallet address so a new vault (e.g. sandbox) gets fresh defaults
264
+ useEffect(() => {
265
+ if (
266
+ !state?.configured ||
267
+ !state?.isUnlocked ||
268
+ workspaceLoading ||
269
+ workspaceApps.length > 0
270
+ ) return;
271
+
272
+ const coldWallet = state.wallets.find(w => w.tier === 'cold');
273
+ const seedKey = `defaultAppsSeeded:${coldWallet?.address || 'unknown'}`;
274
+
275
+ if (localStorage.getItem(seedKey)) return;
276
+
277
+ // 1. Getting Started
278
+ const dismissKey = `setupWizardDismissed:${coldWallet?.address || 'unknown'}`;
279
+ if (!localStorage.getItem(dismissKey)) {
280
+ addApp('setup', undefined, { x: 20, y: 20 });
281
+ }
282
+
283
+ // 2. Agent Chat
284
+ addApp(
285
+ 'installed:agent-chat',
286
+ { appPath: 'agent-chat', appName: 'Agent Chat' },
287
+ { x: 460, y: 20 }
288
+ );
289
+
290
+ // 3. Wallet detail for cold wallet
291
+ if (coldWallet) {
292
+ addApp(
293
+ 'walletDetail',
294
+ {
295
+ walletAddress: coldWallet.address,
296
+ walletName: coldWallet.name,
297
+ walletEmoji: coldWallet.emoji,
298
+ walletColor: coldWallet.color,
299
+ },
300
+ { x: 820, y: 20 },
301
+ `walletDetail-${coldWallet.address}`
302
+ );
303
+ }
304
+
305
+ localStorage.setItem(seedKey, 'true');
306
+ }, [state?.configured, state?.isUnlocked, state?.wallets, workspaceLoading, workspaceApps.length, addApp]);
307
+
308
+ const handleExportSeed = async (e: React.FormEvent) => {
309
+ e.preventDefault();
310
+ setError('');
311
+ setExporting(true);
312
+ try {
313
+ const data = await api.post<{ success: boolean; error?: string; mnemonic?: string }>(Api.Wallet, '/wallet/export-seed', { password: exportPassword });
314
+ if (!data.success) throw new Error(data.error);
315
+ setExportedSeed(data.mnemonic ?? null);
316
+ setExportPassword('');
317
+ } catch (err) {
318
+ setError(err instanceof Error ? err.message : 'Export failed');
319
+ } finally {
320
+ setExporting(false);
321
+ }
322
+ };
323
+
324
+ const copyAddress = (address: string) => {
325
+ navigator.clipboard.writeText(address);
326
+ setCopied(address);
327
+ setTimeout(() => setCopied(null), 2000);
328
+ };
329
+
330
+ const handleNuke = async () => {
331
+ if (!confirmNuke) {
332
+ setConfirmNuke(true);
333
+ return;
334
+ }
335
+ setNuking(true);
336
+ try {
337
+ const data = await api.post<{ success: boolean; error?: string }>(Api.Wallet, '/nuke');
338
+ if (!data.success) throw new Error(data.error);
339
+ setState(null);
340
+ setActiveDrawer(null);
341
+ setConfirmNuke(false);
342
+ window.location.reload();
343
+ } catch (err) {
344
+ setError(err instanceof Error ? err.message : 'Nuke failed');
345
+ } finally {
346
+ setNuking(false);
347
+ }
348
+ };
349
+
350
+ const handleImportSeed = async (e: React.FormEvent) => {
351
+ e.preventDefault();
352
+ if (importPassword !== importConfirmPassword) {
353
+ setError('Passwords do not match');
354
+ return;
355
+ }
356
+ if (importPassword.length < 8) {
357
+ setError('Password must be at least 8 characters');
358
+ return;
359
+ }
360
+ if (!importSeedPhrase.trim()) {
361
+ setError('Please enter a seed phrase');
362
+ return;
363
+ }
364
+
365
+ setImporting(true);
366
+ try {
367
+ const data = await api.post<{ success?: boolean; error?: string }>(Api.Wallet, '/nuke/import', {
368
+ mnemonic: importSeedPhrase.trim(),
369
+ password: importPassword
370
+ });
371
+ if (!data.success && data.error) throw new Error(data.error);
372
+
373
+ setShowImportSeedModal(false);
374
+ setImportSeedPhrase('');
375
+ setImportPassword('');
376
+ setImportConfirmPassword('');
377
+ window.location.reload();
378
+ } catch (err) {
379
+ setError(err instanceof Error ? err.message : 'Import failed');
380
+ } finally {
381
+ setImporting(false);
382
+ }
383
+ };
384
+
385
+ const handleWalletClick = (wallet: WalletData) => {
386
+ const appId = `walletDetail-${wallet.address}`;
387
+
388
+ // Check if app for this wallet is already open
389
+ const existingApp = workspaceApps.find(w => w.id === appId);
390
+ if (existingApp) {
391
+ // Bring to front
392
+ bringToFront(appId);
393
+ return;
394
+ }
395
+
396
+ // Count existing wallet detail apps for offset
397
+ const walletAppCount = workspaceApps.filter(w => w.appType === 'walletDetail').length;
398
+ const offset = walletAppCount * 30;
399
+
400
+ // Add app through workspace system with wallet info for title/color
401
+ addApp(
402
+ 'walletDetail',
403
+ {
404
+ walletAddress: wallet.address,
405
+ walletName: wallet.name,
406
+ walletEmoji: wallet.emoji,
407
+ walletColor: wallet.color,
408
+ },
409
+ { x: 360 + offset, y: 20 + offset },
410
+ appId
411
+ );
412
+ };
413
+
414
+
415
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
416
+ const handleWalletUpdate = async (
417
+ address: string,
418
+ updates: { name?: string; color?: string; emoji?: string; description?: string; hidden?: boolean }
419
+ ) => {
420
+ try {
421
+ const data = await api.post<{ success: boolean; error?: string }>(Api.Wallet, '/wallet/rename', { address, ...updates });
422
+ if (!data.success) throw new Error(data.error);
423
+
424
+ // Update local state - wallet detail apps read from state.wallets
425
+ if (state?.wallets) {
426
+ const updatedWallets = state.wallets.map((w) =>
427
+ w.address === address ? { ...w, ...updates } : w
428
+ );
429
+ setState((prev) => (prev ? { ...prev, wallets: updatedWallets } : null));
430
+ }
431
+ } catch (err) {
432
+ setError(err instanceof Error ? err.message : 'Failed to update wallet');
433
+ }
434
+ };
435
+
436
+ const handleRemoveChain = async (chain: string) => {
437
+ try {
438
+ await removeChainOverride(chain);
439
+ } catch {
440
+ setError('Failed to remove chain');
441
+ }
442
+ };
443
+
444
+ // Add new API key
445
+ const handleAddApiKey = async () => {
446
+ if (!newApiKey.service || !newApiKey.name || !newApiKey.key) {
447
+ setError('Service, name, and key are required');
448
+ return;
449
+ }
450
+
451
+ setSavingApiKey(true);
452
+ try {
453
+ const data = await api.post<{ success: boolean; error?: string }>(Api.Wallet, '/apikeys', {
454
+ service: newApiKey.service.toLowerCase(),
455
+ name: newApiKey.name,
456
+ key: newApiKey.key
457
+ });
458
+
459
+ if (data.success) {
460
+ // Refresh API keys list from AuthContext
461
+ await refreshApiKeys();
462
+ setNewApiKey({ service: '', name: '', key: '' });
463
+ setShowAddApiKeyPopover(false);
464
+ } else {
465
+ setError(data.error || 'Failed to save API key');
466
+ }
467
+ } catch (err) {
468
+ setError(err instanceof Error ? err.message : 'Failed to save API key');
469
+ } finally {
470
+ setSavingApiKey(false);
471
+ }
472
+ };
473
+
474
+ // Delete API key
475
+ const handleDeleteApiKey = async (id: string) => {
476
+ setDeletingApiKey(id);
477
+ try {
478
+ const data = await api.delete<{ success: boolean; error?: string }>(Api.Wallet, `/apikeys/${id}`);
479
+
480
+ if (data.success) {
481
+ await refreshApiKeys();
482
+ } else {
483
+ setError(data.error || 'Failed to delete API key');
484
+ }
485
+ } catch (err) {
486
+ setError(err instanceof Error ? err.message : 'Failed to delete API key');
487
+ } finally {
488
+ setDeletingApiKey(null);
489
+ }
490
+ };
491
+
492
+ // Add Alchemy API key directly (for quick setup)
493
+ const handleAddAlchemyKey = async (key: string): Promise<boolean> => {
494
+ try {
495
+ const data = await api.post<{ success: boolean; error?: string }>(Api.Wallet, '/apikeys', {
496
+ service: 'alchemy',
497
+ name: 'default',
498
+ key
499
+ });
500
+
501
+ if (data.success) {
502
+ await refreshApiKeys();
503
+ return true;
504
+ } else {
505
+ setError(data.error || 'Failed to save Alchemy key');
506
+ return false;
507
+ }
508
+ } catch (err) {
509
+ setError(err instanceof Error ? err.message : 'Failed to save Alchemy key');
510
+ return false;
511
+ }
512
+ };
513
+
514
+ const handleAddChain = async () => {
515
+ const chainName = newChain.name.toLowerCase().trim();
516
+ if (!chainName || !newChain.chainId) {
517
+ setError('Chain name and chain ID are required');
518
+ return;
519
+ }
520
+
521
+ const chainId = parseInt(newChain.chainId, 10);
522
+ const knownChain = KNOWN_CHAINS[chainName];
523
+ const explorer = newChain.explorer || knownChain?.explorer || '';
524
+
525
+ // If RPC is blank and we have Alchemy key + known chain, construct Alchemy URL
526
+ let rpc = newChain.rpc.trim();
527
+ if (!rpc) {
528
+ const alchemyKey = getApiKey('alchemy');
529
+ if (alchemyKey && knownChain?.alchemyPath) {
530
+ rpc = `https://${knownChain.alchemyPath}.g.alchemy.com/v2/${alchemyKey}`;
531
+ } else {
532
+ setError('RPC URL is required (or add Alchemy key for known chains)');
533
+ return;
534
+ }
535
+ }
536
+
537
+ try {
538
+ await saveChainOverride(chainName, { rpc, chainId, explorer });
539
+ setNewChain({ name: '', chainId: '', rpc: '', explorer: '', nativeCurrency: 'ETH' });
540
+ setShowAddChainModal(false);
541
+ } catch {
542
+ setError('Failed to add chain');
543
+ }
544
+ };
545
+
546
+ const handleSaveCustomRpc = async (chain: string, rpc: string) => {
547
+ if (!rpc) {
548
+ setError('RPC URL is required');
549
+ return;
550
+ }
551
+ try {
552
+ // Get current chain config to preserve chainId and explorer
553
+ const currentConfig = chains[chain];
554
+ await saveChainOverride(chain, {
555
+ rpc,
556
+ chainId: currentConfig?.chainId || 0,
557
+ explorer: currentConfig?.explorer || '',
558
+ });
559
+ setEditingChainRpc(null);
560
+ setCustomRpc('');
561
+ } catch {
562
+ setError('Failed to save RPC override');
563
+ }
564
+ };
565
+
566
+ const fetchBackups = async () => {
567
+ // Skip if no auth token
568
+ if (!token) {
569
+ setBackups([]);
570
+ return;
571
+ }
572
+ setBackupsLoading(true);
573
+ try {
574
+ const data = await api.get<{ success: boolean; backups: Array<{ filename: string; timestamp: string; size: number; date: string }> }>(Api.Wallet, '/backup');
575
+ if (data.success) {
576
+ setBackups(data.backups);
577
+ }
578
+ } catch (err) {
579
+ console.error('Failed to fetch backups:', err);
580
+ setBackups([]);
581
+ } finally {
582
+ setBackupsLoading(false);
583
+ }
584
+ };
585
+
586
+ const handleCreateBackup = async () => {
587
+ setCreatingBackup(true);
588
+ try {
589
+ const data = await api.post<{ success: boolean; error?: string }>(Api.Wallet, '/backup');
590
+ if (data.success) {
591
+ await fetchBackups();
592
+ } else {
593
+ setError(data.error || 'Failed to create backup');
594
+ }
595
+ } catch (err) {
596
+ setError(err instanceof Error ? err.message : 'Failed to create backup');
597
+ } finally {
598
+ setCreatingBackup(false);
599
+ }
600
+ };
601
+
602
+ const handleRestoreBackup = async (filename: string) => {
603
+ setRestoringBackup(filename);
604
+ try {
605
+ const data = await api.put<{ success: boolean; error?: string }>(Api.Wallet, '/backup', { filename });
606
+ if (data.success) {
607
+ window.location.reload();
608
+ } else {
609
+ setError(data.error || 'Failed to restore backup');
610
+ }
611
+ } catch (err) {
612
+ setError(err instanceof Error ? err.message : 'Failed to restore backup');
613
+ } finally {
614
+ setRestoringBackup(null);
615
+ }
616
+ };
617
+
618
+ const handleNewTab = () => {
619
+ createWorkspace('NEW');
620
+ };
621
+
622
+ const handleCloseTab = (tabId: string) => {
623
+ deleteWorkspace(tabId);
624
+ };
625
+
626
+ const handleTabChange = (tabId: string) => {
627
+ switchWorkspace(tabId);
628
+ };
629
+
630
+ const handleAppPositionChange = (id: string, pos: AppPosition) => {
631
+ // Update context (will sync to DB)
632
+ updateApp(id, { x: pos.x, y: pos.y });
633
+ };
634
+
635
+ const handleAppLockChange = (id: string, locked: boolean) => {
636
+ updateApp(id, { isLocked: locked });
637
+ };
638
+
639
+ const handleAppSizeChange = (id: string, size: { width: number; height: number }) => {
640
+ updateApp(id, { width: size.width, height: size.height });
641
+ };
642
+
643
+ const handleBringToFront = (id: string) => {
644
+ bringToFront(id);
645
+ };
646
+
647
+ const handleDismissApp = (id: string) => {
648
+ removeApp(id);
649
+ };
650
+
651
+ const handleOpenLogs = () => {
652
+ // Check if logs app already exists
653
+ const logsApp = workspaceApps.find(w => w.appType === 'logs');
654
+ if (logsApp) {
655
+ bringToFront(logsApp.id);
656
+ } else {
657
+ addApp('logs', undefined, { x: 700, y: 20 });
658
+ }
659
+ };
660
+
661
+ const handleOpenApp = (type: string, position?: { x: number; y: number }) => {
662
+ const existing = workspaceApps.find(w => w.appType === type);
663
+ if (existing) {
664
+ bringToFront(existing.id);
665
+ } else {
666
+ addApp(type, undefined, position);
667
+ }
668
+ };
669
+
670
+ const handleAddAppFromStore = (appType: string, config?: Record<string, unknown>) => {
671
+ addApp(appType, config, { x: 360, y: 20 });
672
+ setShowAppStore(false);
673
+ };
674
+
675
+ // Loading state
676
+ if (loading) {
677
+ return (
678
+ <div className="min-h-screen flex items-center justify-center bg-[var(--color-background,#f5f5f5)] relative">
679
+ <div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
680
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-border-muted,#e5e5e5)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-border-muted,#e5e5e5)_1px,transparent_1px)] bg-[size:4rem_4rem] opacity-30" />
681
+ </div>
682
+ <div className="font-mono text-sm text-[var(--color-text-muted,#6b7280)] animate-pulse relative z-10">INITIALIZING SYSTEM...</div>
683
+ </div>
684
+ );
685
+ }
686
+
687
+ // Seed backup screen
688
+ if (seedPhrase && !seedConfirmed) {
689
+ return (
690
+ <div className="min-h-screen flex items-center justify-center p-4 bg-[var(--color-background,#f5f5f5)] relative">
691
+ <div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
692
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-border-muted,#e5e5e5)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-border-muted,#e5e5e5)_1px,transparent_1px)] bg-[size:4rem_4rem] opacity-30" />
693
+ </div>
694
+ <div className="w-full max-w-md bg-[var(--color-surface,#ffffff)] border border-[var(--color-border,#d4d4d8)] relative z-10">
695
+ <div className="absolute top-2 left-2 w-4 h-4 border-l-2 border-t-2 border-[var(--color-border-focus,#0a0a0a)]" />
696
+ <div className="absolute top-2 right-2 w-4 h-4 border-r-2 border-t-2 border-[var(--color-border-focus,#0a0a0a)]" />
697
+ <div className="absolute bottom-2 left-2 w-4 h-4 border-l-2 border-b-2 border-[var(--color-border-focus,#0a0a0a)]" />
698
+ <div className="absolute bottom-2 right-2 w-4 h-4 border-r-2 border-b-2 border-[var(--color-border-focus,#0a0a0a)]" />
699
+ <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]" />
700
+
701
+ <div className="p-8 relative z-10">
702
+ <div className="flex items-center gap-3 mb-6">
703
+ <div className="w-10 h-10 bg-[var(--color-warning,#ff4d00)] flex items-center justify-center">
704
+ <AlertTriangle size={16} className="text-white" />
705
+ </div>
706
+ <div>
707
+ <div className="font-mono text-[10px] text-[var(--color-warning,#ff4d00)] tracking-widest">CRITICAL</div>
708
+ <div className="font-black text-xl text-[var(--color-text,#0a0a0a)] tracking-tight">BACKUP SEED PHRASE</div>
709
+ </div>
710
+ </div>
711
+
712
+ <div className="mb-4 p-3 bg-[var(--color-warning,#ff4d00)]/10 border border-[var(--color-warning,#ff4d00)]/30">
713
+ <div className="font-mono text-[10px] text-[var(--color-warning,#ff4d00)] leading-relaxed">
714
+ Write this down and store it safely. This is the ONLY way to recover your wallets. It will NOT be shown again.
715
+ </div>
716
+ </div>
717
+
718
+ <div className="mb-6 p-4 bg-[var(--color-text,#0a0a0a)] border-2 border-[var(--color-border-focus,#0a0a0a)] relative">
719
+ <button
720
+ onClick={() => {
721
+ navigator.clipboard.writeText(seedPhrase);
722
+ setCopied('seed');
723
+ setTimeout(() => setCopied(null), 2000);
724
+ }}
725
+ className="absolute top-2 right-2 p-1.5 bg-[var(--color-text,#0a0a0a)]/80 hover:bg-[var(--color-text,#0a0a0a)]/70 transition-colors"
726
+ >
727
+ <Copy size={12} className={copied === 'seed' ? 'text-[var(--color-accent,#ccff00)]' : 'text-[var(--color-text-muted,#6b7280)]'} />
728
+ </button>
729
+ <div className="font-mono text-sm text-[var(--color-accent,#ccff00)] leading-relaxed break-words select-all">
730
+ {seedPhrase}
731
+ </div>
732
+ </div>
733
+
734
+ <div className="space-y-3">
735
+ <label className="flex items-start gap-3 cursor-pointer group">
736
+ <input
737
+ type="checkbox"
738
+ checked={seedConfirmed}
739
+ onChange={(e) => setSeedConfirmed(e.target.checked)}
740
+ className="mt-1 w-4 h-4 accent-[var(--color-accent,#ccff00)]"
741
+ />
742
+ <span className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)] leading-relaxed group-hover:text-[var(--color-text,#0a0a0a)]">
743
+ I have saved my seed phrase in a secure location.
744
+ </span>
745
+ </label>
746
+
747
+ <button
748
+ onClick={() => {
749
+ setSeedPhrase(null);
750
+ fetchState();
751
+ }}
752
+ disabled={!seedConfirmed}
753
+ className="w-full h-14 bg-[var(--color-text,#0a0a0a)] text-white relative overflow-hidden disabled:opacity-30 disabled:cursor-not-allowed group"
754
+ >
755
+ <span className="relative z-10 font-mono font-bold text-xs uppercase tracking-[0.15em] group-hover:text-[var(--color-accent,#ccff00)] transition-colors">
756
+ CONTINUE
757
+ </span>
758
+ </button>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ </div>
763
+ );
764
+ }
765
+
766
+ // Derived state for other components
767
+ const coldWallets = state?.wallets.filter(w => w.tier === 'cold') || [];
768
+ const isLocked = state?.configured && !state?.isUnlocked;
769
+ const isConfigured = state?.configured ?? false;
770
+
771
+ // MAIN LAYOUT - Sidebar + Tab Content
772
+ return (
773
+ <div className="h-screen flex bg-[var(--color-background,#f5f5f5)] overflow-hidden">
774
+ {/* Background Pattern with AURA/MAXXING */}
775
+ <div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
776
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-border-muted,#e5e5e5)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-border-muted,#e5e5e5)_1px,transparent_1px)] bg-[size:4rem_4rem] opacity-30" />
777
+ <div className="absolute inset-0 tyvek-texture opacity-40 mix-blend-multiply" />
778
+ <div className="absolute top-[5%] left-[30%] opacity-[0.03] select-none">
779
+ <div className="text-[15vw] font-black leading-none text-[var(--color-text,#0a0a0a)] font-mono tracking-tighter">AURA</div>
780
+ </div>
781
+ <div className="absolute bottom-[5%] right-[5%] opacity-[0.03] select-none">
782
+ <div className="text-[12vw] font-black leading-none text-[var(--color-text,#0a0a0a)] font-mono tracking-tighter text-right">MAXXING</div>
783
+ </div>
784
+ {/* Lab Markings */}
785
+ <div className="absolute top-10 left-[290px] w-24 h-24 border-l-4 border-t-4 border-[var(--color-text,#0a0a0a)] opacity-10">
786
+ <div className="absolute top-2 left-2 w-3 h-3 bg-[var(--color-text,#0a0a0a)]" />
787
+ </div>
788
+ <div className="absolute bottom-10 right-10 w-24 h-24 border-r-4 border-b-4 border-[var(--color-text,#0a0a0a)] opacity-10 flex items-end justify-end">
789
+ <div className="absolute bottom-2 right-2 w-3 h-3 bg-[var(--color-text,#0a0a0a)]" />
790
+ </div>
791
+ </div>
792
+
793
+ {/* Wallet Sidebar - Self-contained component */}
794
+ <WalletSidebar
795
+ onSend={() => handleOpenApp('send', { x: 360, y: 20 })}
796
+ onReceive={() => setActiveDrawer('receive')}
797
+ onLogs={handleOpenLogs}
798
+ onAgentKeys={() => handleOpenApp('agentKeys', { x: 360, y: 20 })}
799
+ onAppStore={() => setShowAppStore(true)}
800
+ onWalletClick={handleWalletClick}
801
+ onImportSeed={() => setShowImportSeedModal(true)}
802
+ onSettings={() => { setActiveDrawer(activeDrawer === 'settings' ? null : 'settings'); setConfirmNuke(false); }}
803
+ pendingActionCount={requests.length}
804
+ onStateChange={(newState) => {
805
+ setState(prev => prev ? {
806
+ ...prev,
807
+ configured: newState.configured,
808
+ isUnlocked: newState.unlocked,
809
+ wallets: newState.wallets,
810
+ } : {
811
+ configured: newState.configured,
812
+ isUnlocked: newState.unlocked,
813
+ wallets: newState.wallets,
814
+ });
815
+ }}
816
+ />
817
+
818
+ {/* Main Content Area */}
819
+ <div className="flex-1 flex flex-col relative z-10">
820
+ {/* Error Toast */}
821
+ {error && (
822
+ <div className="absolute top-2 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 px-3 py-2 bg-[var(--color-surface,#ffffff)] border border-[var(--color-warning,#ff4d00)] shadow-lg">
823
+ <span className="font-mono text-[10px] text-[var(--color-warning,#ff4d00)]">{error}</span>
824
+ <button onClick={() => setError('')} className="text-[var(--color-warning,#ff4d00)] hover:text-[var(--color-warning,#ff4d00)]/70">
825
+ <X size={10} />
826
+ </button>
827
+ </div>
828
+ )}
829
+
830
+ {isLocked || !isConfigured ? (
831
+ /* Locked/unconfigured state - no workspace, no tabs, no apps */
832
+ <div className="flex-1 flex items-center justify-center">
833
+ <div className="text-center">
834
+ <Lock size={32} className="mx-auto mb-3 text-[var(--color-text-faint,#9ca3af)]" />
835
+ <div className="font-mono text-sm text-[var(--color-text-muted,#6b7280)]">
836
+ {!isConfigured ? 'WALLET NOT CONFIGURED' : 'VAULT LOCKED'}
837
+ </div>
838
+ <div className="font-mono text-[10px] text-[var(--color-text-faint,#9ca3af)] mt-1">
839
+ {!isConfigured ? 'Set up your wallet using the sidebar' : 'Unlock your wallet to access the workspace'}
840
+ </div>
841
+ </div>
842
+ </div>
843
+ ) : (
844
+ <>
845
+ {/* Tab Bar - only shown when unlocked */}
846
+ <TabBar
847
+ tabs={tabs}
848
+ activeTab={activeWorkspaceId}
849
+ onTabChange={handleTabChange}
850
+ onTabClose={handleCloseTab}
851
+ onNewTab={handleNewTab}
852
+ onTabUpdate={handleTabUpdate}
853
+ onTidy={tidyApps}
854
+ onAppStore={() => setShowAppStore(true)}
855
+ notifications={notifications}
856
+ onDismissNotification={dismissNotification}
857
+ />
858
+
859
+ {/* Content Area - Freeform Canvas */}
860
+ <div className="flex-1 relative overflow-y-auto overflow-x-hidden">
861
+ {workspaceLoading ? (
862
+ <div className="absolute inset-0 flex items-center justify-center">
863
+ <div className="text-center">
864
+ <Loader2 size={24} className="mx-auto mb-2 text-[var(--color-text-faint,#9ca3af)] animate-spin" />
865
+ <div className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">LOADING WORKSPACE...</div>
866
+ </div>
867
+ </div>
868
+ ) : (
869
+ <div className="relative w-full h-full min-h-[800px]">
870
+ {/* Render apps from context */}
871
+ {workspaceApps.filter(w => w.isVisible).map((app) => (
872
+ <WorkspaceApp
873
+ key={app.id}
874
+ app={app}
875
+ onPositionChange={handleAppPositionChange}
876
+ onSizeChange={handleAppSizeChange}
877
+ onLockChange={handleAppLockChange}
878
+ onBringToFront={handleBringToFront}
879
+ onDismiss={handleDismissApp}
880
+ />
881
+ ))}
882
+
883
+ {/* Empty state */}
884
+ {workspaceApps.length === 0 && (
885
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
886
+ <div className="text-center">
887
+ <div className="font-mono text-sm text-[var(--color-text-muted,#6b7280)]">EMPTY WORKSPACE</div>
888
+ <div className="font-mono text-[10px] text-[var(--color-text-faint,#9ca3af)] mt-1">Apps can be added via WebSocket or sidebar</div>
889
+ </div>
890
+ </div>
891
+ )}
892
+ </div>
893
+ )}
894
+ </div>
895
+
896
+ <HumanActionBar
897
+ requests={requests}
898
+ resolveAction={resolveAction}
899
+ actionLoading={actionLoading}
900
+ />
901
+ </>
902
+ )}
903
+ </div>
904
+
905
+ {/* Settings Drawer */}
906
+ <Drawer
907
+ isOpen={activeDrawer === 'settings'}
908
+ onClose={() => setActiveDrawer(null)}
909
+ title="SETTINGS"
910
+ subtitle="System configuration"
911
+ >
912
+ <SettingsContent
913
+ chains={chains}
914
+ editingChainRpc={editingChainRpc}
915
+ setEditingChainRpc={setEditingChainRpc}
916
+ customRpc={customRpc}
917
+ setCustomRpc={setCustomRpc}
918
+ savingConfig={savingConfig}
919
+ handleSaveCustomRpc={handleSaveCustomRpc}
920
+ handleRemoveChain={handleRemoveChain}
921
+ exportedSeed={exportedSeed}
922
+ setExportedSeed={setExportedSeed}
923
+ exportPassword={exportPassword}
924
+ setExportPassword={setExportPassword}
925
+ exporting={exporting}
926
+ handleExportSeed={handleExportSeed}
927
+ copied={copied}
928
+ setCopied={setCopied}
929
+ confirmNuke={confirmNuke}
930
+ nuking={nuking}
931
+ handleNuke={handleNuke}
932
+ backups={backups}
933
+ backupsLoading={backupsLoading}
934
+ creatingBackup={creatingBackup}
935
+ restoringBackup={restoringBackup}
936
+ onFetchBackups={fetchBackups}
937
+ onCreateBackup={handleCreateBackup}
938
+ onRestoreBackup={handleRestoreBackup}
939
+ showAddChainPopover={showAddChainModal}
940
+ addChainAnchorEl={addChainAnchorEl}
941
+ onOpenAddChain={(el) => { setAddChainAnchorEl(el); setShowAddChainModal(true); }}
942
+ onCloseAddChain={() => { setShowAddChainModal(false); setAddChainAnchorEl(null); }}
943
+ newChain={newChain}
944
+ setNewChain={setNewChain}
945
+ handleAddChain={handleAddChain}
946
+ // Chain overrides props
947
+ chainOverrides={chainOverrides}
948
+ hasAlchemyKey={!!getApiKey('alchemy')}
949
+ // Auth state
950
+ isUnlocked={state?.isUnlocked ?? false}
951
+ // API Keys props (from AuthContext)
952
+ apiKeys={authApiKeys}
953
+ apiKeysLoading={authApiKeysLoading}
954
+ showAddApiKeyPopover={showAddApiKeyPopover}
955
+ addApiKeyAnchorEl={addApiKeyAnchorEl}
956
+ onOpenAddApiKey={(el) => { setAddApiKeyAnchorEl(el); setShowAddApiKeyPopover(true); }}
957
+ onCloseAddApiKey={() => { setShowAddApiKeyPopover(false); setAddApiKeyAnchorEl(null); }}
958
+ newApiKey={newApiKey}
959
+ setNewApiKey={setNewApiKey}
960
+ savingApiKey={savingApiKey}
961
+ handleAddApiKey={handleAddApiKey}
962
+ deletingApiKey={deletingApiKey}
963
+ handleDeleteApiKey={handleDeleteApiKey}
964
+ onAddAlchemyKey={handleAddAlchemyKey}
965
+ />
966
+ </Drawer>
967
+
968
+ {/* Receive Drawer */}
969
+ <Drawer
970
+ isOpen={activeDrawer === 'receive'}
971
+ onClose={() => setActiveDrawer(null)}
972
+ title="RECEIVE"
973
+ subtitle="Fund your wallets"
974
+ >
975
+ <ReceiveContent
976
+ coldWallets={coldWallets}
977
+ copyAddress={copyAddress}
978
+ copied={copied}
979
+ />
980
+ </Drawer>
981
+
982
+ {/* App Store Drawer - only accessible when unlocked */}
983
+ <AppStoreDrawer
984
+ isOpen={showAppStore && !isLocked && isConfigured}
985
+ onClose={() => setShowAppStore(false)}
986
+ onAddApp={handleAddAppFromStore}
987
+ />
988
+
989
+ {/* Import Seed Modal */}
990
+ <Modal
991
+ isOpen={showImportSeedModal}
992
+ onClose={() => {
993
+ setShowImportSeedModal(false);
994
+ setImportSeedPhrase('');
995
+ setImportPassword('');
996
+ setImportConfirmPassword('');
997
+ }}
998
+ title="Import Seed Phrase"
999
+ subtitle="Recovery"
1000
+ icon={<KeyRound size={20} className="text-[#0047ff]" />}
1001
+ size="md"
1002
+ >
1003
+ <form onSubmit={handleImportSeed} className="space-y-4">
1004
+ <div>
1005
+ <label className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)] tracking-widest block mb-1">
1006
+ SEED_PHRASE
1007
+ </label>
1008
+ <textarea
1009
+ value={importSeedPhrase}
1010
+ onChange={(e) => setImportSeedPhrase(e.target.value)}
1011
+ placeholder="Enter your 12 or 24 word seed phrase..."
1012
+ rows={3}
1013
+ className="w-full px-3 py-2 border border-[var(--color-border,#d4d4d8)] font-mono text-xs focus:outline-none focus:border-[var(--color-border-focus,#0a0a0a)] bg-[var(--color-surface,#ffffff)] text-[var(--color-text,#0a0a0a)] resize-none"
1014
+ />
1015
+ </div>
1016
+ <div>
1017
+ <label className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)] tracking-widest block mb-1">
1018
+ NEW_PASSWORD
1019
+ </label>
1020
+ <input
1021
+ type="password"
1022
+ value={importPassword}
1023
+ onChange={(e) => setImportPassword(e.target.value)}
1024
+ placeholder="Min 8 characters"
1025
+ className="w-full px-3 py-2 border border-[var(--color-border,#d4d4d8)] font-mono text-xs focus:outline-none focus:border-[var(--color-border-focus,#0a0a0a)] bg-[var(--color-surface,#ffffff)] text-[var(--color-text,#0a0a0a)]"
1026
+ />
1027
+ </div>
1028
+ <div>
1029
+ <label className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)] tracking-widest block mb-1">
1030
+ CONFIRM_PASSWORD
1031
+ </label>
1032
+ <input
1033
+ type="password"
1034
+ value={importConfirmPassword}
1035
+ onChange={(e) => setImportConfirmPassword(e.target.value)}
1036
+ placeholder="Confirm password"
1037
+ className="w-full px-3 py-2 border border-[var(--color-border,#d4d4d8)] font-mono text-xs focus:outline-none focus:border-[var(--color-border-focus,#0a0a0a)] bg-[var(--color-surface,#ffffff)] text-[var(--color-text,#0a0a0a)]"
1038
+ />
1039
+ </div>
1040
+ <div className="p-3 bg-[var(--color-info,#0047ff)]/5 border border-[var(--color-info,#0047ff)]/30">
1041
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
1042
+ This will restore your wallet from the seed phrase. All hot wallets will be re-derived from this seed.
1043
+ </div>
1044
+ </div>
1045
+ <div className="flex gap-2">
1046
+ <button
1047
+ type="button"
1048
+ onClick={() => {
1049
+ setShowImportSeedModal(false);
1050
+ setImportSeedPhrase('');
1051
+ setImportPassword('');
1052
+ setImportConfirmPassword('');
1053
+ }}
1054
+ className="flex-1 h-10 border border-[var(--color-border,#d4d4d8)] font-mono text-[10px] tracking-widest text-[var(--color-text-muted,#6b7280)] hover:border-[var(--color-border-focus,#0a0a0a)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
1055
+ >
1056
+ CANCEL
1057
+ </button>
1058
+ <button
1059
+ type="submit"
1060
+ disabled={importing || !importSeedPhrase || !importPassword || !importConfirmPassword}
1061
+ className="flex-1 h-10 bg-[var(--color-text,#0a0a0a)] text-white font-mono text-[10px] tracking-widest flex items-center justify-center gap-2 disabled:opacity-50 hover:text-[var(--color-accent,#ccff00)] transition-colors"
1062
+ >
1063
+ {importing ? <Loader2 size={12} className="animate-spin" /> : <KeyRound size={12} />}
1064
+ {importing ? 'IMPORTING...' : 'IMPORT'}
1065
+ </button>
1066
+ </div>
1067
+ </form>
1068
+ </Modal>
1069
+ </div>
1070
+ );
1071
+ }
1072
+
1073
+ // WorkspaceApp - Renders a app from the workspace context
1074
+ // All apps are self-contained and only receive config
1075
+ interface WorkspaceAppProps {
1076
+ app: WorkspaceAppState;
1077
+ onPositionChange: (id: string, pos: { x: number; y: number }) => void;
1078
+ onSizeChange: (id: string, size: { width: number; height: number }) => void;
1079
+ onLockChange: (id: string, locked: boolean) => void;
1080
+ onBringToFront: (id: string) => void;
1081
+ onDismiss: (id: string) => void;
1082
+ }
1083
+
1084
+ function WorkspaceApp({
1085
+ app,
1086
+ onPositionChange,
1087
+ onSizeChange,
1088
+ onLockChange,
1089
+ onBringToFront,
1090
+ onDismiss,
1091
+ }: WorkspaceAppProps) {
1092
+ const [refreshKey, setRefreshKey] = useState(0);
1093
+ const isThirdParty = app.appType.startsWith('installed:');
1094
+ const definition = getAppDefinition(app.appType);
1095
+
1096
+ if (!definition) {
1097
+ return (
1098
+ <DraggableApp
1099
+ id={app.id}
1100
+ title={`UNKNOWN: ${app.appType}`}
1101
+ icon={Code}
1102
+ color="gray"
1103
+ initialPosition={{ x: app.x, y: app.y }}
1104
+ initialSize={{ width: app.width, height: app.height }}
1105
+ locked={app.isLocked}
1106
+ onLockChange={onLockChange}
1107
+ dismissable
1108
+ onDismiss={() => onDismiss(app.id)}
1109
+ onPositionChange={onPositionChange}
1110
+ onSizeChange={onSizeChange}
1111
+ onBringToFront={onBringToFront}
1112
+ zIndex={app.zIndex}
1113
+ >
1114
+ <div className="py-4 text-center">
1115
+ <Code size={20} className="mx-auto mb-2 text-[var(--color-text-faint,#9ca3af)]" />
1116
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">UNKNOWN APP TYPE</div>
1117
+ <div className="font-mono text-[8px] text-[var(--color-text-faint,#9ca3af)] mt-1">{app.appType}</div>
1118
+ </div>
1119
+ </DraggableApp>
1120
+ );
1121
+ }
1122
+
1123
+ const AppComponent = definition.component;
1124
+
1125
+ // All apps are self-contained - just pass config
1126
+ const renderContent = () => {
1127
+ const config = isThirdParty
1128
+ ? {
1129
+ appPath: app.appType.slice(10), // installed:agent-chat -> agent-chat
1130
+ ...app.config, // Explicit config (if present) wins
1131
+ _refreshKey: refreshKey,
1132
+ }
1133
+ : app.config;
1134
+ return (
1135
+ <Suspense fallback={<AppLoading />}>
1136
+ <AppComponent config={config} />
1137
+ </Suspense>
1138
+ );
1139
+ };
1140
+
1141
+ // Get title for wallet detail apps (use config passed when app was opened)
1142
+ const getTitle = () => {
1143
+ if (app.appType === 'walletDetail') {
1144
+ const emoji = app.config?.walletEmoji as string | undefined;
1145
+ const name = app.config?.walletName as string | undefined;
1146
+ if (emoji || name) {
1147
+ return emoji ? `${emoji} ${name || 'WALLET'}` : (name || 'WALLET');
1148
+ }
1149
+ }
1150
+ return definition.title;
1151
+ };
1152
+
1153
+ // Get color for wallet detail apps (use config passed when app was opened)
1154
+ const getColor = (): AppColor => {
1155
+ if (app.appType === 'walletDetail') {
1156
+ const color = app.config?.walletColor as string | undefined;
1157
+ if (color) {
1158
+ const colorMap: Record<string, AppColor> = {
1159
+ '#ff4d00': 'orange',
1160
+ '#0047ff': 'blue',
1161
+ '#00c853': 'teal',
1162
+ '#ffab00': 'orange',
1163
+ '#9c27b0': 'purple',
1164
+ '#00bcd4': 'teal',
1165
+ '#e91e63': 'rose',
1166
+ '#607d8b': 'gray',
1167
+ };
1168
+ return colorMap[color] || 'orange';
1169
+ }
1170
+ }
1171
+ return definition.color;
1172
+ };
1173
+
1174
+ // Get subtitle for iframe apps (show URL hostname)
1175
+ const getSubtitle = () => {
1176
+ if (app.appType === 'iframe' && app.config?.url) {
1177
+ try {
1178
+ const url = new URL(app.config.url as string);
1179
+ return url.hostname;
1180
+ } catch {
1181
+ return undefined;
1182
+ }
1183
+ }
1184
+ return undefined;
1185
+ };
1186
+
1187
+ const getSubtitleLink = () => {
1188
+ if (app.appType === 'iframe' && app.config?.url) {
1189
+ return app.config.url as string;
1190
+ }
1191
+ return undefined;
1192
+ };
1193
+
1194
+ return (
1195
+ <DraggableApp
1196
+ id={app.id}
1197
+ title={getTitle()}
1198
+ subtitle={getSubtitle()}
1199
+ subtitleLink={getSubtitleLink()}
1200
+ icon={definition.icon}
1201
+ color={getColor()}
1202
+ initialPosition={{ x: app.x, y: app.y }}
1203
+ initialSize={{ width: app.width, height: app.height }}
1204
+ locked={app.isLocked}
1205
+ onLockChange={onLockChange}
1206
+ onRefresh={isThirdParty ? () => setRefreshKey(k => k + 1) : undefined}
1207
+ dismissable
1208
+ onDismiss={() => onDismiss(app.id)}
1209
+ onPositionChange={onPositionChange}
1210
+ onSizeChange={onSizeChange}
1211
+ onBringToFront={onBringToFront}
1212
+ zIndex={app.zIndex}
1213
+ >
1214
+ {renderContent()}
1215
+ </DraggableApp>
1216
+ );
1217
+ }
1218
+
1219
+ function AppLoading() {
1220
+ return (
1221
+ <div className="py-6 text-center">
1222
+ <Loader2 size={20} className="mx-auto mb-2 text-[var(--color-text-faint,#9ca3af)] animate-spin" />
1223
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">LOADING...</div>
1224
+ </div>
1225
+ );
1226
+ }
1227
+
1228
+ interface BackupInfo {
1229
+ filename: string;
1230
+ timestamp: string;
1231
+ size: number;
1232
+ date: string;
1233
+ }
1234
+
1235
+ function SettingsContent({
1236
+ chains,
1237
+ editingChainRpc,
1238
+ setEditingChainRpc,
1239
+ customRpc,
1240
+ setCustomRpc,
1241
+ savingConfig,
1242
+ handleSaveCustomRpc,
1243
+ handleRemoveChain,
1244
+ exportedSeed,
1245
+ setExportedSeed,
1246
+ exportPassword,
1247
+ setExportPassword,
1248
+ exporting,
1249
+ handleExportSeed,
1250
+ copied,
1251
+ setCopied,
1252
+ confirmNuke,
1253
+ nuking,
1254
+ handleNuke,
1255
+ backups,
1256
+ backupsLoading,
1257
+ creatingBackup,
1258
+ restoringBackup,
1259
+ onFetchBackups,
1260
+ onCreateBackup,
1261
+ onRestoreBackup,
1262
+ showAddChainPopover,
1263
+ addChainAnchorEl,
1264
+ onOpenAddChain,
1265
+ onCloseAddChain,
1266
+ newChain,
1267
+ setNewChain,
1268
+ handleAddChain,
1269
+ // Chain overrides props
1270
+ chainOverrides,
1271
+ hasAlchemyKey,
1272
+ // Auth state
1273
+ isUnlocked,
1274
+ // API Keys props
1275
+ apiKeys,
1276
+ apiKeysLoading,
1277
+ showAddApiKeyPopover,
1278
+ addApiKeyAnchorEl,
1279
+ onOpenAddApiKey,
1280
+ onCloseAddApiKey,
1281
+ newApiKey,
1282
+ setNewApiKey,
1283
+ savingApiKey,
1284
+ handleAddApiKey,
1285
+ deletingApiKey,
1286
+ handleDeleteApiKey,
1287
+ onAddAlchemyKey,
1288
+ }: {
1289
+ chains: Record<string, { rpc: string; chainId: number; explorer: string; nativeCurrency: string }>;
1290
+ editingChainRpc: string | null;
1291
+ setEditingChainRpc: (c: string | null) => void;
1292
+ customRpc: string;
1293
+ setCustomRpc: (r: string) => void;
1294
+ savingConfig: boolean;
1295
+ handleSaveCustomRpc: (chain: string, rpc: string) => void;
1296
+ handleRemoveChain: (chain: string) => void;
1297
+ exportedSeed: string | null;
1298
+ setExportedSeed: (s: string | null) => void;
1299
+ exportPassword: string;
1300
+ setExportPassword: (p: string) => void;
1301
+ exporting: boolean;
1302
+ handleExportSeed: (e: React.FormEvent) => void;
1303
+ copied: string | null;
1304
+ setCopied: (c: string | null) => void;
1305
+ confirmNuke: boolean;
1306
+ nuking: boolean;
1307
+ handleNuke: () => void;
1308
+ backups: BackupInfo[];
1309
+ backupsLoading: boolean;
1310
+ creatingBackup: boolean;
1311
+ restoringBackup: string | null;
1312
+ onFetchBackups: () => void;
1313
+ onCreateBackup: () => void;
1314
+ onRestoreBackup: (filename: string) => void;
1315
+ showAddChainPopover: boolean;
1316
+ addChainAnchorEl: HTMLElement | null;
1317
+ onOpenAddChain: (el: HTMLElement) => void;
1318
+ onCloseAddChain: () => void;
1319
+ newChain: { name: string; chainId: string; rpc: string; explorer: string; nativeCurrency: string };
1320
+ setNewChain: (c: { name: string; chainId: string; rpc: string; explorer: string; nativeCurrency: string }) => void;
1321
+ // Chain overrides types
1322
+ chainOverrides: Record<string, ChainConfig>;
1323
+ hasAlchemyKey: boolean;
1324
+ // Auth state
1325
+ isUnlocked: boolean;
1326
+ // API Keys types
1327
+ apiKeys: ApiKey[];
1328
+ apiKeysLoading: boolean;
1329
+ showAddApiKeyPopover: boolean;
1330
+ addApiKeyAnchorEl: HTMLElement | null;
1331
+ onOpenAddApiKey: (el: HTMLElement) => void;
1332
+ onCloseAddApiKey: () => void;
1333
+ newApiKey: { service: string; name: string; key: string };
1334
+ setNewApiKey: (k: { service: string; name: string; key: string }) => void;
1335
+ savingApiKey: boolean;
1336
+ handleAddApiKey: () => void;
1337
+ deletingApiKey: string | null;
1338
+ handleDeleteApiKey: (id: string) => void;
1339
+ handleAddChain: () => void;
1340
+ onAddAlchemyKey: (key: string) => Promise<boolean>;
1341
+ }) {
1342
+ const [restoreConfirmOpen, setRestoreConfirmOpen] = React.useState<string | null>(null);
1343
+ const [restoreAnchorEl, setRestoreAnchorEl] = React.useState<HTMLElement | null>(null);
1344
+ const [alchemyKeyInput, setAlchemyKeyInput] = React.useState('');
1345
+ const [addingAlchemyKey, setAddingAlchemyKey] = React.useState(false);
1346
+
1347
+ // Fetch backups when component mounts (only if unlocked)
1348
+ React.useEffect(() => {
1349
+ if (isUnlocked) {
1350
+ onFetchBackups();
1351
+ }
1352
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1353
+ }, [isUnlocked]);
1354
+
1355
+ // Handle adding Alchemy API key
1356
+ const handleAlchemyKeySubmit = async () => {
1357
+ if (!alchemyKeyInput.trim()) return;
1358
+ setAddingAlchemyKey(true);
1359
+ const success = await onAddAlchemyKey(alchemyKeyInput.trim());
1360
+ if (success) {
1361
+ setAlchemyKeyInput('');
1362
+ }
1363
+ setAddingAlchemyKey(false);
1364
+ };
1365
+
1366
+ const formatBackupDate = (timestamp: string) => {
1367
+ // timestamp format: YYYYMMDD_HHMMSS
1368
+ const year = timestamp.slice(0, 4);
1369
+ const month = timestamp.slice(4, 6);
1370
+ const day = timestamp.slice(6, 8);
1371
+ const hour = timestamp.slice(9, 11);
1372
+ const minute = timestamp.slice(11, 13);
1373
+ return `${year}-${month}-${day} ${hour}:${minute}`;
1374
+ };
1375
+
1376
+ const formatSize = (bytes: number) => {
1377
+ if (bytes < 1024) return `${bytes} B`;
1378
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1379
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1380
+ };
1381
+ // Alchemy-supported chains
1382
+ const alchemyChains = ['base', 'ethereum', 'arbitrum', 'optimism'];
1383
+
1384
+ // Get the RPC source for a chain (override, alchemy, or public)
1385
+ const getRpcSource = (chainName: string): 'override' | 'alchemy' | 'public' => {
1386
+ if (chainOverrides[chainName]) return 'override';
1387
+ if (hasAlchemyKey && alchemyChains.includes(chainName)) return 'alchemy';
1388
+ return 'public';
1389
+ };
1390
+
1391
+ const [agentSectionOpen, setAgentSectionOpen] = React.useState(false);
1392
+ const [agentTier, setAgentTier] = React.useState<string>('admin');
1393
+ const [agentTierLoading, setAgentTierLoading] = React.useState(true);
1394
+ const [agentTierSaving, setAgentTierSaving] = React.useState(false);
1395
+
1396
+ // Fetch agent tier on mount
1397
+ React.useEffect(() => {
1398
+ (async () => {
1399
+ try {
1400
+ const grouped = await api.get<Record<string, Array<{ key: string; value: unknown }>>>(Api.Wallet, '/defaults');
1401
+ const permsGroup = grouped.permissions || [];
1402
+ const tierRow = permsGroup.find((r: { key: string }) => r.key === 'permissions.agent_tier');
1403
+ if (tierRow) setAgentTier(tierRow.value as string);
1404
+ } catch { /* use default */ }
1405
+ finally { setAgentTierLoading(false); }
1406
+ })();
1407
+ }, []);
1408
+
1409
+ const handleTierChange = async (tier: string) => {
1410
+ setAgentTier(tier);
1411
+ setAgentTierSaving(true);
1412
+ try {
1413
+ await api.patch(Api.Wallet, `/defaults/${encodeURIComponent('permissions.agent_tier')}`, { value: tier });
1414
+ } catch { /* revert on error */ setAgentTier(tier === 'admin' ? 'restricted' : 'admin'); }
1415
+ finally { setAgentTierSaving(false); }
1416
+ };
1417
+
1418
+ const [systemDefaultsOpen, setSystemDefaultsOpen] = React.useState(false);
1419
+ const [rpcOpen, setRpcOpen] = React.useState(false);
1420
+ const [apiKeysOpen, setApiKeysOpen] = React.useState(false);
1421
+ const [exportSeedOpen, setExportSeedOpen] = React.useState(false);
1422
+ const [backupOpen, setBackupOpen] = React.useState(false);
1423
+ const [dangerOpen, setDangerOpen] = React.useState(false);
1424
+
1425
+ return (
1426
+ <div className="space-y-4">
1427
+ {/* DEFAULT_AGENT — permission tier + AI model */}
1428
+ <div className="bg-[var(--color-surface)] border border-[var(--color-border)]">
1429
+ <button
1430
+ onClick={() => setAgentSectionOpen(!agentSectionOpen)}
1431
+ className="w-full p-4 flex items-center justify-between cursor-pointer hover:bg-[var(--color-background-alt)] transition-colors"
1432
+ >
1433
+ <div className="flex items-center gap-2">
1434
+ <Bot size={12} className="text-[var(--color-text-muted)]" />
1435
+ <span className="font-mono text-[10px] text-[var(--color-text-muted)] tracking-widest">DEFAULT_AGENT</span>
1436
+ </div>
1437
+ <ChevronDown
1438
+ size={12}
1439
+ className={`text-[var(--color-text-muted)] transition-transform ${agentSectionOpen ? 'rotate-180' : ''}`}
1440
+ />
1441
+ </button>
1442
+ {agentSectionOpen && (
1443
+ <div className="p-4 pt-0 space-y-4">
1444
+ {/* Permission Tier Toggle */}
1445
+ <div className="p-3 space-y-2" style={{ border: '1px solid var(--color-border)', background: 'var(--color-surface)' }}>
1446
+ <div className="font-mono text-[10px]" style={{ color: 'var(--color-text)' }}>Permission Tier</div>
1447
+ <div className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted)' }}>
1448
+ Controls what the agent-chat app can do directly
1449
+ </div>
1450
+ {agentTierLoading ? (
1451
+ <div className="flex items-center gap-2 py-2">
1452
+ <Loader2 size={12} className="animate-spin" style={{ color: 'var(--color-text-muted)' }} />
1453
+ </div>
1454
+ ) : (
1455
+ <div className="flex gap-2">
1456
+ <button
1457
+ onClick={() => handleTierChange('admin')}
1458
+ disabled={agentTierSaving}
1459
+ className="flex-1 p-2 font-mono text-[9px] text-left transition-colors"
1460
+ style={{
1461
+ border: agentTier === 'admin' ? '1px solid var(--color-accent, #ccff00)' : '1px solid var(--color-border)',
1462
+ background: agentTier === 'admin' ? 'var(--color-background-alt)' : 'transparent',
1463
+ color: agentTier === 'admin' ? 'var(--color-text)' : 'var(--color-text-muted)',
1464
+ opacity: agentTierSaving ? 0.5 : 1,
1465
+ }}
1466
+ >
1467
+ <div className="font-bold">Full Admin</div>
1468
+ <div className="text-[8px]" style={{ color: 'var(--color-text-muted)' }}>Agent can do everything directly</div>
1469
+ </button>
1470
+ <button
1471
+ onClick={() => handleTierChange('restricted')}
1472
+ disabled={agentTierSaving}
1473
+ className="flex-1 p-2 font-mono text-[9px] text-left transition-colors"
1474
+ style={{
1475
+ border: agentTier === 'restricted' ? '1px solid var(--color-accent, #ccff00)' : '1px solid var(--color-border)',
1476
+ background: agentTier === 'restricted' ? 'var(--color-background-alt)' : 'transparent',
1477
+ color: agentTier === 'restricted' ? 'var(--color-text)' : 'var(--color-text-muted)',
1478
+ opacity: agentTierSaving ? 0.5 : 1,
1479
+ }}
1480
+ >
1481
+ <div className="font-bold">Restricted</div>
1482
+ <div className="text-[8px]" style={{ color: 'var(--color-text-muted)' }}>Approval required for actions</div>
1483
+ </button>
1484
+ </div>
1485
+ )}
1486
+ </div>
1487
+
1488
+ {/* AI Model Selection (moved from SystemDefaults) */}
1489
+ <AiEngineSection />
1490
+ </div>
1491
+ )}
1492
+ </div>
1493
+
1494
+ {/* System Defaults (limits, permissions, AI engine) — collapsible */}
1495
+ <div className="bg-[var(--color-surface)] border border-[var(--color-border)]">
1496
+ <button
1497
+ onClick={() => setSystemDefaultsOpen(!systemDefaultsOpen)}
1498
+ className="w-full p-4 flex items-center justify-between cursor-pointer hover:bg-[var(--color-background-alt)] transition-colors"
1499
+ >
1500
+ <div className="flex items-center gap-2">
1501
+ <Settings size={12} className="text-[var(--color-text-muted)]" />
1502
+ <span className="font-mono text-[10px] text-[var(--color-text-muted)] tracking-widest">SYSTEM_DEFAULTS</span>
1503
+ </div>
1504
+ <ChevronDown
1505
+ size={12}
1506
+ className={`text-[var(--color-text-muted)] transition-transform ${systemDefaultsOpen ? 'rotate-180' : ''}`}
1507
+ />
1508
+ </button>
1509
+ {systemDefaultsOpen && <SystemDefaults />}
1510
+ </div>
1511
+
1512
+ {/* RPC Configuration */}
1513
+ <div className="bg-[var(--color-surface)] border border-[var(--color-border)]">
1514
+ <button
1515
+ onClick={() => setRpcOpen(!rpcOpen)}
1516
+ className="w-full p-4 flex items-center justify-between cursor-pointer hover:bg-[var(--color-background-alt)] transition-colors"
1517
+ >
1518
+ <div className="flex items-center gap-2">
1519
+ <Code size={12} className="text-[var(--color-text-muted)]" />
1520
+ <span className="font-mono text-[10px] text-[var(--color-text-muted)] tracking-widest">RPC_CONFIGURATION</span>
1521
+ </div>
1522
+ <ChevronDown
1523
+ size={12}
1524
+ className={`text-[var(--color-text-muted)] transition-transform ${rpcOpen ? 'rotate-180' : ''}`}
1525
+ />
1526
+ </button>
1527
+ {rpcOpen && <div className="p-4 pt-0">
1528
+ <div className="p-3 bg-[var(--color-background-alt)] border border-[var(--color-border)] space-y-4">
1529
+ {/* Quick Setup - Alchemy API Key */}
1530
+ <div className="p-3 border border-dashed border-[var(--color-border)] bg-[var(--color-surface)]">
1531
+ <div className="font-mono text-[9px] text-[var(--color-text-muted)] uppercase tracking-widest mb-2">ALCHEMY API KEY</div>
1532
+ {hasAlchemyKey ? (
1533
+ <div className="space-y-2">
1534
+ <div className="flex items-center gap-2">
1535
+ <Check size={12} className="text-[var(--color-success)]" />
1536
+ <span className="font-mono text-[10px] text-[var(--color-success)]">Configured</span>
1537
+ <span className="font-mono text-[8px] text-[var(--color-text-muted)]">
1538
+ (auto-configures: {alchemyChains.join(', ')})
1539
+ </span>
1540
+ </div>
1541
+ <div className="font-mono text-[8px] text-[var(--color-text-faint)] leading-relaxed">
1542
+ Remove in API Keys section below. Custom overrides take priority.
1543
+ </div>
1544
+ </div>
1545
+ ) : (
1546
+ <div className="space-y-2">
1547
+ <div className="font-mono text-[8px] text-[var(--color-text-muted)] leading-relaxed mb-2">
1548
+ Get a free key at alchemy.com to auto-configure: {alchemyChains.join(', ')}
1549
+ </div>
1550
+ <div className="flex gap-2">
1551
+ <div className="flex-1">
1552
+ <TextInput
1553
+ label=""
1554
+ type="password"
1555
+ value={alchemyKeyInput}
1556
+ onChange={(e) => setAlchemyKeyInput(e.target.value)}
1557
+ placeholder="Paste your Alchemy API key..."
1558
+ compact
1559
+ />
1560
+ </div>
1561
+ <Button
1562
+ size="sm"
1563
+ onClick={handleAlchemyKeySubmit}
1564
+ disabled={addingAlchemyKey || !alchemyKeyInput.trim()}
1565
+ loading={addingAlchemyKey}
1566
+ icon={!addingAlchemyKey ? <Plus size={10} /> : undefined}
1567
+ >
1568
+ ADD
1569
+ </Button>
1570
+ </div>
1571
+ <div className="font-mono text-[8px] text-[var(--color-text-faint)] leading-relaxed">
1572
+ Currently using public RPCs (may have rate limits).
1573
+ </div>
1574
+ </div>
1575
+ )}
1576
+ </div>
1577
+
1578
+ {/* Chain Overrides */}
1579
+ <div>
1580
+ <div className="flex items-center justify-between mb-2">
1581
+ <div className="font-mono text-[9px] text-[var(--color-text-muted)] uppercase tracking-widest">CHAIN OVERRIDES</div>
1582
+ <div className="relative">
1583
+ <Button
1584
+ variant="ghost"
1585
+ size="sm"
1586
+ onClick={(e) => onOpenAddChain(e.currentTarget)}
1587
+ icon={<Plus size={10} />}
1588
+ >
1589
+ ADD
1590
+ </Button>
1591
+ <Popover
1592
+ isOpen={showAddChainPopover}
1593
+ onClose={onCloseAddChain}
1594
+ title="ADD_CHAIN"
1595
+ anchorEl={addChainAnchorEl}
1596
+ anchor="right"
1597
+ className="w-64"
1598
+ >
1599
+ <div className="space-y-3">
1600
+ <div className="font-mono text-[8px] text-[var(--color-text-muted)] leading-relaxed">
1601
+ Known chains: arbitrum, optimism, polygon, zksync
1602
+ </div>
1603
+ <TextInput
1604
+ label="NAME"
1605
+ type="text"
1606
+ value={newChain.name}
1607
+ onChange={(e) => setNewChain({ ...newChain, name: e.target.value })}
1608
+ placeholder="arbitrum, polygon, zksync..."
1609
+ compact
1610
+ />
1611
+ <TextInput
1612
+ label="CHAIN_ID"
1613
+ type="number"
1614
+ value={newChain.chainId}
1615
+ onChange={(e) => setNewChain({ ...newChain, chainId: e.target.value })}
1616
+ placeholder="42161, 10, 137..."
1617
+ compact
1618
+ />
1619
+ <TextInput
1620
+ label="RPC (optional)"
1621
+ type="text"
1622
+ value={newChain.rpc}
1623
+ onChange={(e) => setNewChain({ ...newChain, rpc: e.target.value })}
1624
+ placeholder="blank = use Alchemy"
1625
+ compact
1626
+ />
1627
+ <Button
1628
+ size="md"
1629
+ onClick={() => { handleAddChain(); onCloseAddChain(); }}
1630
+ disabled={savingConfig || !newChain.name || !newChain.chainId}
1631
+ loading={savingConfig}
1632
+ icon={!savingConfig ? <Plus size={10} /> : undefined}
1633
+ className="w-full"
1634
+ >
1635
+ ADD CHAIN
1636
+ </Button>
1637
+ </div>
1638
+ </Popover>
1639
+ </div>
1640
+ </div>
1641
+ <div className="space-y-2">
1642
+ {Object.entries(chains).map(([chainName, chainConfig]) => {
1643
+ const source = getRpcSource(chainName);
1644
+ // Can remove any chain except defaults (base, ethereum)
1645
+ const canRemove = !['base', 'ethereum'].includes(chainName);
1646
+ return (
1647
+ <div key={chainName} className="flex items-center gap-2 p-2 bg-[var(--color-surface)] border border-[var(--color-border)]">
1648
+ <div className="w-20 font-mono text-[10px] font-bold text-[var(--color-text)] uppercase flex items-center gap-1">
1649
+ {chainName}
1650
+ <span className={`font-mono text-[7px] px-1 py-0.5 rounded ${
1651
+ source === 'override' ? 'bg-[var(--color-accent)]/20 text-[var(--color-accent)]' :
1652
+ source === 'alchemy' ? 'bg-[var(--color-success)]/20 text-[var(--color-success)]' :
1653
+ 'bg-[var(--color-text-muted)]/20 text-[var(--color-text-muted)]'
1654
+ }`}>
1655
+ {source === 'override' ? 'CUSTOM' : source === 'alchemy' ? 'ALCHEMY' : 'PUBLIC'}
1656
+ </span>
1657
+ </div>
1658
+ {editingChainRpc === chainName ? (
1659
+ <div className="flex-1">
1660
+ <TextInput
1661
+ label=""
1662
+ type="text"
1663
+ value={customRpc}
1664
+ onChange={(e) => setCustomRpc(e.target.value)}
1665
+ placeholder="https://..."
1666
+ compact
1667
+ autoFocus
1668
+ rightElement={
1669
+ <div className="flex gap-1">
1670
+ <Button variant="ghost" size="sm" onClick={() => handleSaveCustomRpc(chainName, customRpc)} icon={<Check size={12} className="text-[var(--color-success)]" />}>
1671
+ {''}
1672
+ </Button>
1673
+ <Button variant="ghost" size="sm" onClick={() => { setEditingChainRpc(null); setCustomRpc(''); }} icon={<X size={12} />}>
1674
+ {''}
1675
+ </Button>
1676
+ </div>
1677
+ }
1678
+ />
1679
+ </div>
1680
+ ) : (
1681
+ <>
1682
+ <div className="flex-1" />
1683
+ <Button variant="secondary" size="sm" onClick={() => { setEditingChainRpc(chainName); setCustomRpc(chainConfig.rpc); }}>
1684
+ EDIT RPC
1685
+ </Button>
1686
+ {canRemove && (
1687
+ <Button variant="ghost" size="sm" onClick={() => handleRemoveChain(chainName)} icon={<Trash2 size={10} />} className="hover:text-[var(--color-warning)]">
1688
+ {''}
1689
+ </Button>
1690
+ )}
1691
+ </>
1692
+ )}
1693
+ </div>
1694
+ );
1695
+ })}
1696
+ </div>
1697
+ </div>
1698
+ </div>
1699
+ </div>}
1700
+ </div>
1701
+
1702
+ {/* API Keys */}
1703
+ <div className="bg-[var(--color-surface)] border border-[var(--color-border)]">
1704
+ <button
1705
+ onClick={() => setApiKeysOpen(!apiKeysOpen)}
1706
+ className="w-full p-4 flex items-center justify-between cursor-pointer hover:bg-[var(--color-background-alt)] transition-colors"
1707
+ >
1708
+ <div className="flex items-center gap-2">
1709
+ <KeyRound size={12} className="text-[var(--color-text-muted)]" />
1710
+ <span className="font-mono text-[10px] text-[var(--color-text-muted)] tracking-widest">API_KEYS</span>
1711
+ </div>
1712
+ <ChevronDown
1713
+ size={12}
1714
+ className={`text-[var(--color-text-muted)] transition-transform ${apiKeysOpen ? 'rotate-180' : ''}`}
1715
+ />
1716
+ </button>
1717
+ {apiKeysOpen && <div className="p-4 pt-0">
1718
+ <div className="p-3 bg-[var(--color-background-alt)] border border-[var(--color-border)] space-y-3">
1719
+ <div className="flex items-center justify-between">
1720
+ <div className="font-mono text-[9px] text-[var(--color-text-muted)]">
1721
+ Store API keys for premium services (Alchemy, Infura, etc.)
1722
+ </div>
1723
+ <div className="relative">
1724
+ <Button
1725
+ variant="ghost"
1726
+ size="sm"
1727
+ onClick={(e) => onOpenAddApiKey(e.currentTarget)}
1728
+ icon={<Plus size={10} />}
1729
+ >
1730
+ ADD
1731
+ </Button>
1732
+ <Popover
1733
+ isOpen={showAddApiKeyPopover}
1734
+ onClose={onCloseAddApiKey}
1735
+ title="ADD_API_KEY"
1736
+ anchorEl={addApiKeyAnchorEl}
1737
+ anchor="right"
1738
+ className="w-72"
1739
+ >
1740
+ <div className="space-y-3">
1741
+ <TextInput
1742
+ label="SERVICE"
1743
+ type="text"
1744
+ value={newApiKey.service}
1745
+ onChange={(e) => setNewApiKey({ ...newApiKey, service: e.target.value })}
1746
+ placeholder="alchemy, infura, etherscan..."
1747
+ compact
1748
+ />
1749
+ <TextInput
1750
+ label="NAME"
1751
+ type="text"
1752
+ value={newApiKey.name}
1753
+ onChange={(e) => setNewApiKey({ ...newApiKey, name: e.target.value })}
1754
+ placeholder="My API Key"
1755
+ compact
1756
+ />
1757
+ <TextInput
1758
+ label="API_KEY"
1759
+ type="password"
1760
+ value={newApiKey.key}
1761
+ onChange={(e) => setNewApiKey({ ...newApiKey, key: e.target.value })}
1762
+ placeholder="Enter your API key..."
1763
+ compact
1764
+ />
1765
+ <Button
1766
+ size="md"
1767
+ onClick={handleAddApiKey}
1768
+ disabled={savingApiKey || !newApiKey.service || !newApiKey.name || !newApiKey.key}
1769
+ loading={savingApiKey}
1770
+ icon={!savingApiKey ? <Plus size={10} /> : undefined}
1771
+ className="w-full"
1772
+ >
1773
+ ADD KEY
1774
+ </Button>
1775
+ </div>
1776
+ </Popover>
1777
+ </div>
1778
+ </div>
1779
+
1780
+ {apiKeysLoading ? (
1781
+ <div className="py-4 flex items-center justify-center">
1782
+ <Loader2 size={16} className="animate-spin text-[var(--color-text-muted)]" />
1783
+ </div>
1784
+ ) : apiKeys.length === 0 ? (
1785
+ <div className="py-4 text-center border border-dashed border-[var(--color-border)]">
1786
+ <div className="font-mono text-[9px] text-[var(--color-text-muted)]">No API keys stored</div>
1787
+ <div className="font-mono text-[8px] text-[var(--color-text-faint)] mt-1">Add keys for Alchemy, Infura, etc.</div>
1788
+ </div>
1789
+ ) : (
1790
+ <div className="space-y-2">
1791
+ {apiKeys.map((apiKey) => (
1792
+ <div key={apiKey.id} className="flex items-center gap-2 p-2 bg-[var(--color-surface)] border border-[var(--color-border)]">
1793
+ <div className="flex-1 min-w-0">
1794
+ <div className="flex items-center gap-2">
1795
+ <span className="font-mono text-[10px] font-bold text-[var(--color-text)] uppercase">{apiKey.service}</span>
1796
+ <span className="font-mono text-[9px] text-[var(--color-text-muted)]">{apiKey.name}</span>
1797
+ </div>
1798
+ <div className="font-mono text-[8px] text-[var(--color-text-faint)] truncate">
1799
+ {apiKey.keyMasked || apiKey.key}
1800
+ </div>
1801
+ </div>
1802
+ <Button
1803
+ variant="ghost"
1804
+ size="sm"
1805
+ onClick={() => handleDeleteApiKey(apiKey.id)}
1806
+ disabled={deletingApiKey === apiKey.id}
1807
+ icon={deletingApiKey === apiKey.id ? <Loader2 size={10} className="animate-spin" /> : <Trash2 size={10} />}
1808
+ className="hover:text-[var(--color-warning)]"
1809
+ >
1810
+ {''}
1811
+ </Button>
1812
+ </div>
1813
+ ))}
1814
+ </div>
1815
+ )}
1816
+
1817
+ <div className="pt-2 border-t border-dashed border-[var(--color-border)]">
1818
+ <div className="font-mono text-[8px] text-[var(--color-text-faint)] leading-relaxed">
1819
+ Alchemy keys will automatically configure RPCs for supported chains.
1820
+ </div>
1821
+ </div>
1822
+ </div>
1823
+ </div>}
1824
+ </div>
1825
+
1826
+ {/* Export Seed */}
1827
+ <div className="bg-[var(--color-surface)] border border-[var(--color-border)]">
1828
+ <button
1829
+ onClick={() => setExportSeedOpen(!exportSeedOpen)}
1830
+ className="w-full p-4 flex items-center justify-between cursor-pointer hover:bg-[var(--color-background-alt)] transition-colors"
1831
+ >
1832
+ <div className="flex items-center gap-2">
1833
+ <Shield size={12} className="text-[var(--color-text-muted)]" />
1834
+ <span className="font-mono text-[10px] text-[var(--color-text-muted)] tracking-widest">EXPORT_SEED</span>
1835
+ </div>
1836
+ <ChevronDown
1837
+ size={12}
1838
+ className={`text-[var(--color-text-muted)] transition-transform ${exportSeedOpen ? 'rotate-180' : ''}`}
1839
+ />
1840
+ </button>
1841
+ {exportSeedOpen && <div className="p-4 pt-0">
1842
+ {exportedSeed ? (
1843
+ <div className="space-y-3">
1844
+ <div className="p-3 bg-[var(--color-text)] border border-[var(--color-border-focus)] relative">
1845
+ <Button
1846
+ variant="ghost"
1847
+ size="sm"
1848
+ onClick={() => { navigator.clipboard.writeText(exportedSeed); setCopied('exported'); setTimeout(() => setCopied(null), 2000); }}
1849
+ className="absolute top-2 right-2 bg-[var(--color-text-muted)]/20 hover:bg-[var(--color-text-muted)]/40"
1850
+ icon={<Copy size={10} className={copied === 'exported' ? 'text-[var(--color-accent)]' : 'text-[var(--color-surface)]'} />}
1851
+ >
1852
+ {''}
1853
+ </Button>
1854
+ <div className="font-mono text-xs text-[var(--color-accent)] leading-relaxed break-words select-all pr-8">{exportedSeed}</div>
1855
+ </div>
1856
+ <Button variant="ghost" size="sm" onClick={() => setExportedSeed(null)}>
1857
+ HIDE
1858
+ </Button>
1859
+ </div>
1860
+ ) : (
1861
+ <form onSubmit={handleExportSeed}>
1862
+ <TextInput
1863
+ label="PASSWORD"
1864
+ type="password"
1865
+ value={exportPassword}
1866
+ onChange={(e) => setExportPassword(e.target.value)}
1867
+ placeholder="Enter password to export..."
1868
+ compact
1869
+ rightElement={
1870
+ <Button type="submit" size="sm" disabled={exporting || !exportPassword} loading={exporting}>
1871
+ EXPORT
1872
+ </Button>
1873
+ }
1874
+ />
1875
+ </form>
1876
+ )}
1877
+ </div>}
1878
+ </div>
1879
+
1880
+ {/* Database Backup */}
1881
+ <div className="bg-[var(--color-surface)] border border-[var(--color-border)]">
1882
+ <button
1883
+ onClick={() => setBackupOpen(!backupOpen)}
1884
+ className="w-full p-4 flex items-center justify-between cursor-pointer hover:bg-[var(--color-background-alt)] transition-colors"
1885
+ >
1886
+ <div className="flex items-center gap-2">
1887
+ <Database size={12} className="text-[var(--color-text-muted)]" />
1888
+ <span className="font-mono text-[10px] text-[var(--color-text-muted)] tracking-widest">DATABASE_BACKUP</span>
1889
+ </div>
1890
+ <ChevronDown
1891
+ size={12}
1892
+ className={`text-[var(--color-text-muted)] transition-transform ${backupOpen ? 'rotate-180' : ''}`}
1893
+ />
1894
+ </button>
1895
+ {backupOpen && <div className="p-4 pt-0">
1896
+ <div className="space-y-3">
1897
+ <Button
1898
+ variant="secondary"
1899
+ size="lg"
1900
+ onClick={onCreateBackup}
1901
+ disabled={creatingBackup}
1902
+ loading={creatingBackup}
1903
+ icon={!creatingBackup ? <Plus size={12} /> : undefined}
1904
+ className="w-full"
1905
+ >
1906
+ {creatingBackup ? 'CREATING...' : 'CREATE BACKUP'}
1907
+ </Button>
1908
+
1909
+ {backupsLoading ? (
1910
+ <div className="py-4 flex items-center justify-center">
1911
+ <Loader2 size={16} className="animate-spin text-[var(--color-text-muted)]" />
1912
+ </div>
1913
+ ) : backups.length === 0 ? (
1914
+ <div className="py-4 text-center">
1915
+ <div className="font-mono text-[9px] text-[var(--color-text-muted)]">No backups found</div>
1916
+ </div>
1917
+ ) : (
1918
+ <div className="space-y-1 max-h-48 overflow-y-auto">
1919
+ {backups.map((backup) => (
1920
+ <div key={backup.filename} className="relative">
1921
+ <button
1922
+ onClick={(e) => {
1923
+ setRestoreAnchorEl(e.currentTarget);
1924
+ setRestoreConfirmOpen(backup.filename);
1925
+ }}
1926
+ className="w-full p-2 border border-[var(--color-border)] hover:border-[var(--color-border-focus)] hover:bg-[var(--color-background-alt)] transition-colors text-left flex items-center justify-between group"
1927
+ >
1928
+ <div className="flex items-center gap-2">
1929
+ <RotateCcw size={10} className="text-[var(--color-text-muted)] group-hover:text-[var(--color-text)]" />
1930
+ <span className="font-mono text-[10px] text-[var(--color-text)]">
1931
+ {formatBackupDate(backup.timestamp)}
1932
+ </span>
1933
+ </div>
1934
+ <span className="font-mono text-[9px] text-[var(--color-text-muted)]">
1935
+ {formatSize(backup.size)}
1936
+ </span>
1937
+ </button>
1938
+ <ConfirmationPopover
1939
+ isOpen={restoreConfirmOpen === backup.filename}
1940
+ onClose={() => {
1941
+ setRestoreConfirmOpen(null);
1942
+ setRestoreAnchorEl(null);
1943
+ }}
1944
+ onConfirm={() => {
1945
+ onRestoreBackup(backup.filename);
1946
+ setRestoreConfirmOpen(null);
1947
+ setRestoreAnchorEl(null);
1948
+ }}
1949
+ title="RESTORE"
1950
+ message="Restore to this backup? You will lose all data since this backup was created."
1951
+ confirmLabel="RESTORE"
1952
+ loading={restoringBackup === backup.filename}
1953
+ anchorEl={restoreAnchorEl}
1954
+ anchor="left"
1955
+ />
1956
+ </div>
1957
+ ))}
1958
+ </div>
1959
+ )}
1960
+
1961
+ <div className="pt-2 border-t border-dashed border-[var(--color-border)]">
1962
+ <div className="font-mono text-[8px] text-[var(--color-text-faint)] leading-relaxed">
1963
+ Backups are tied to the current database schema. Restoring after migrations may cause issues.
1964
+ </div>
1965
+ </div>
1966
+ </div>
1967
+ </div>}
1968
+ </div>
1969
+
1970
+ {/* Danger Zone */}
1971
+ <div className="bg-[var(--color-surface)] border border-[var(--color-border)]">
1972
+ <button
1973
+ onClick={() => setDangerOpen(!dangerOpen)}
1974
+ className="w-full p-4 flex items-center justify-between cursor-pointer hover:bg-[var(--color-background-alt)] transition-colors"
1975
+ >
1976
+ <div className="flex items-center gap-2">
1977
+ <AlertTriangle size={12} className="text-[var(--color-warning)]" />
1978
+ <span className="font-mono text-[10px] text-[var(--color-warning)] tracking-widest">DANGER_ZONE</span>
1979
+ </div>
1980
+ <ChevronDown
1981
+ size={12}
1982
+ className={`text-[var(--color-warning)] transition-transform ${dangerOpen ? 'rotate-180' : ''}`}
1983
+ />
1984
+ </button>
1985
+ {dangerOpen && <div className="p-4 pt-0">
1986
+ <div className="p-3 border-2 border-[var(--color-warning)]" style={{ backgroundColor: 'color-mix(in srgb, var(--color-warning) 5%, transparent)' }}>
1987
+ <div className="font-mono text-[10px] text-[var(--color-text-muted)] mb-3">Delete ALL data. Irreversible.</div>
1988
+ <Button
1989
+ variant={confirmNuke ? 'primary' : 'danger'}
1990
+ size="lg"
1991
+ onClick={handleNuke}
1992
+ disabled={nuking}
1993
+ loading={nuking}
1994
+ icon={!nuking ? <Trash2 size={12} /> : undefined}
1995
+ className={`w-full ${confirmNuke ? '!bg-[var(--color-warning)] !border-[var(--color-warning)] hover:!bg-[var(--color-warning)]' : ''}`}
1996
+ >
1997
+ {nuking ? 'NUKING...' : confirmNuke ? 'CONFIRM' : 'NUKE'}
1998
+ </Button>
1999
+ </div>
2000
+ </div>}
2001
+ </div>
2002
+ </div>
2003
+ );
2004
+ }
2005
+
2006
+ function ReceiveContent({
2007
+ coldWallets,
2008
+ copyAddress,
2009
+ copied,
2010
+ }: {
2011
+ coldWallets: WalletData[];
2012
+ copyAddress: (addr: string) => void;
2013
+ copied: string | null;
2014
+ }) {
2015
+ if (coldWallets.length === 0) {
2016
+ return (
2017
+ <div className="text-center py-8">
2018
+ <div className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">No wallet found. Set up your wallet first.</div>
2019
+ </div>
2020
+ );
2021
+ }
2022
+
2023
+ return (
2024
+ <div className="space-y-5">
2025
+ {coldWallets.map((wallet) => (
2026
+ <div key={wallet.address} className="space-y-3">
2027
+ {/* Vault label */}
2028
+ <div className="flex items-center justify-center gap-2">
2029
+ <Shield size={12} style={{ color: 'var(--color-info, #0047ff)' }} />
2030
+ <span className="font-mono text-[9px] font-bold tracking-widest" style={{ color: 'var(--color-info, #0047ff)' }}>
2031
+ {wallet.name?.toUpperCase() || 'VAULT'}
2032
+ {wallet.chain ? ` (${wallet.chain.toUpperCase()})` : ''}
2033
+ </span>
2034
+ </div>
2035
+
2036
+ {/* QR Code - white bg required for scanning */}
2037
+ <div className="flex justify-center">
2038
+ <div
2039
+ className="p-3 relative"
2040
+ style={{
2041
+ backgroundColor: 'var(--color-surface, #ffffff)',
2042
+ border: '1px solid var(--color-border, #d4d4d8)',
2043
+ }}
2044
+ >
2045
+ <div className="absolute top-1 left-1 w-2 h-2 border-l border-t" style={{ borderColor: 'var(--color-border-focus, #0a0a0a)' }} />
2046
+ <div className="absolute top-1 right-1 w-2 h-2 border-r border-t" style={{ borderColor: 'var(--color-border-focus, #0a0a0a)' }} />
2047
+ <div className="absolute bottom-1 left-1 w-2 h-2 border-l border-b" style={{ borderColor: 'var(--color-border-focus, #0a0a0a)' }} />
2048
+ <div className="absolute bottom-1 right-1 w-2 h-2 border-r border-b" style={{ borderColor: 'var(--color-border-focus, #0a0a0a)' }} />
2049
+ <img
2050
+ src={`https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${wallet.address}&bgcolor=ffffff&color=0a0a0a&margin=0`}
2051
+ alt="Wallet QR Code"
2052
+ className="w-40 h-40"
2053
+ />
2054
+ </div>
2055
+ </div>
2056
+
2057
+ {/* Address */}
2058
+ <div
2059
+ className="p-3 relative group cursor-pointer"
2060
+ onClick={() => copyAddress(wallet.address)}
2061
+ style={{
2062
+ backgroundColor: 'var(--color-background-alt, #f4f4f5)',
2063
+ border: '1px solid var(--color-border, #d4d4d8)',
2064
+ }}
2065
+ >
2066
+ <code
2067
+ className="font-mono text-[11px] break-all select-all block text-center leading-relaxed pr-6"
2068
+ style={{ color: 'var(--color-text, #0a0a0a)' }}
2069
+ >
2070
+ {wallet.address}
2071
+ </code>
2072
+ <button
2073
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-1 opacity-60 group-hover:opacity-100 transition-opacity"
2074
+ onClick={(e) => {
2075
+ e.stopPropagation();
2076
+ copyAddress(wallet.address);
2077
+ }}
2078
+ >
2079
+ <Copy size={14} style={{ color: copied === wallet.address ? 'var(--color-success, #00c853)' : 'var(--color-info, #0047ff)' }} />
2080
+ </button>
2081
+ </div>
2082
+ {copied === wallet.address && (
2083
+ <div className="text-center">
2084
+ <span className="font-mono text-[9px]" style={{ color: 'var(--color-success, #00c853)' }}>COPIED TO CLIPBOARD</span>
2085
+ </div>
2086
+ )}
2087
+
2088
+ {/* Divider between vaults */}
2089
+ {coldWallets.length > 1 && wallet !== coldWallets[coldWallets.length - 1] && (
2090
+ <div className="border-t" style={{ borderColor: 'var(--color-border, #d4d4d8)' }} />
2091
+ )}
2092
+ </div>
2093
+ ))}
2094
+
2095
+ {/* Instructions */}
2096
+ <div className="space-y-2 pt-2">
2097
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)] tracking-widest">INSTRUCTIONS</div>
2098
+ <div className="space-y-1.5">
2099
+ <div className="flex items-start gap-2">
2100
+ <div className="w-4 h-4 bg-[var(--color-background-alt,#e8e8e6)] flex items-center justify-center shrink-0 mt-0.5">
2101
+ <span className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">1</span>
2102
+ </div>
2103
+ <span className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">Scan QR or copy address above</span>
2104
+ </div>
2105
+ <div className="flex items-start gap-2">
2106
+ <div className="w-4 h-4 bg-[var(--color-background-alt,#e8e8e6)] flex items-center justify-center shrink-0 mt-0.5">
2107
+ <span className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">2</span>
2108
+ </div>
2109
+ <span className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">Send ETH from exchange or another wallet</span>
2110
+ </div>
2111
+ <div className="flex items-start gap-2">
2112
+ <div className="w-4 h-4 bg-[var(--color-background-alt,#e8e8e6)] flex items-center justify-center shrink-0 mt-0.5">
2113
+ <span className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)]">3</span>
2114
+ </div>
2115
+ <span className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">Funds will appear after network confirmation</span>
2116
+ </div>
2117
+ </div>
2118
+ </div>
2119
+
2120
+ </div>
2121
+ );
2122
+ }