auramaxx 0.0.1

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