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.
- package/LICENSE +26 -0
- package/README.md +77 -0
- package/apps/desktop-electron/main.js +428 -0
- package/bin/auramaxx.js +1063 -0
- package/docs/ADAPTERS.md +466 -0
- package/docs/AGENT_SETUP.md +159 -0
- package/docs/API.md +127 -0
- package/docs/APPS.md +199 -0
- package/docs/ARCHITECTURE.md +235 -0
- package/docs/AUTH.md +318 -0
- package/docs/BEST-PRACTICES.md +82 -0
- package/docs/CLI.md +141 -0
- package/docs/DESKTOP_ELECTRON.md +26 -0
- package/docs/DEVELOPING-APPS.md +453 -0
- package/docs/MCP.md +122 -0
- package/docs/PACKAGING_POLICY.md +19 -0
- package/docs/PERMISSION.md +137 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/README.md +50 -0
- package/docs/SKILLS.md +132 -0
- package/docs/TROUBLESHOOTING.md +376 -0
- package/docs/WORKSPACE.md +673 -0
- package/docs/agent-auth.md +14 -0
- package/docs/api/authentication.md +79 -0
- package/docs/api/secrets/api-keys.md +28 -0
- package/docs/api/secrets/credentials.md +80 -0
- package/docs/api/secrets/sharing.md +48 -0
- package/docs/api/system.md +41 -0
- package/docs/api/wallets/apps-strategies.md +66 -0
- package/docs/api/wallets/core.md +46 -0
- package/docs/api/wallets/data-portfolio.md +42 -0
- package/docs/aura-file.md +48 -0
- package/docs/core-concepts/FEATURES.md +114 -0
- package/docs/credentials.md +120 -0
- package/docs/external/HOW_TO_AURAMAXX/GETTING_SECRETS.md +33 -0
- package/docs/external/HOW_TO_AURAMAXX/README.md +45 -0
- package/docs/external/getting-started.md +10 -0
- package/docs/external/overview.md +19 -0
- package/docs/external/persona-paths.md +7 -0
- package/docs/external/share-secret.md +76 -0
- package/docs/external/why-aura.md +7 -0
- package/docs/security.md +227 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +28 -0
- package/package.json +167 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/20260222090000_update_admin_ttl_default/migration.sql +10 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/actions.ts +474 -0
- package/server/cli/commands/api.ts +220 -0
- package/server/cli/commands/apikey.ts +277 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/auth.ts +464 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/diary.ts +274 -0
- package/server/cli/commands/doctor.ts +1247 -0
- package/server/cli/commands/env.ts +476 -0
- package/server/cli/commands/experimental.ts +69 -0
- package/server/cli/commands/init.ts +798 -0
- package/server/cli/commands/lock.ts +157 -0
- package/server/cli/commands/mcp.ts +285 -0
- package/server/cli/commands/quickhack.ts +86 -0
- package/server/cli/commands/release-check.ts +231 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/service.ts +320 -0
- package/server/cli/commands/shell-hook.ts +512 -0
- package/server/cli/commands/skill.ts +216 -0
- package/server/cli/commands/start.ts +139 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +36 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +50 -0
- package/server/cli/commands/vault.ts +1323 -0
- package/server/cli/commands/wallet.ts +209 -0
- package/server/cli/index.ts +280 -0
- package/server/cli/lib/approval-poll.ts +94 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +280 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/escalation.ts +57 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/lock-unlock-helper.ts +71 -0
- package/server/cli/lib/process.ts +162 -0
- package/server/cli/lib/prompt.ts +294 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +579 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +420 -0
- package/server/lib/adapters/factory.ts +119 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +419 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +258 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/approval-link.ts +27 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/auto-execute.ts +160 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +1048 -0
- package/server/lib/config.ts +408 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +111 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +608 -0
- package/server/lib/credential-scope.ts +102 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +533 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +422 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +58 -0
- package/server/lib/defaults.ts +386 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/diary.ts +34 -0
- package/server/lib/dont-ask-again-policy.ts +41 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +114 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +217 -0
- package/server/lib/feature-flags.ts +93 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/human-action-summary.ts +80 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +340 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +230 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +71 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +299 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +297 -0
- package/server/lib/resolve-action.ts +328 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/secret-gist-share.ts +296 -0
- package/server/lib/sessions.ts +634 -0
- package/server/lib/socket-path.ts +56 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +159 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +237 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +84 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/update-check.ts +35 -0
- package/server/lib/verified-summary.ts +414 -0
- package/server/lib/view-registry.ts +80 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +1589 -0
- package/server/mcp/tools.ts +276 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +539 -0
- package/server/routes/adapters.ts +711 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +343 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +406 -0
- package/server/routes/backup.ts +404 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +380 -0
- package/server/routes/credential-vaults.ts +159 -0
- package/server/routes/credentials.ts +1782 -0
- package/server/routes/dashboard.ts +97 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/flags.ts +11 -0
- package/server/routes/fund.ts +225 -0
- package/server/routes/heartbeat.ts +375 -0
- package/server/routes/import.ts +364 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +366 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +352 -0
- package/server/routes/swap-solana.ts +176 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +467 -0
- package/server/routes/views.ts +41 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +709 -0
- package/server/types.ts +146 -0
- package/shared/credential-field-schema.ts +248 -0
- package/skills/auramaxx/HEARTBEAT.md +78 -0
- package/skills/auramaxx/SKILL.md +745 -0
- package/skills/auramaxx/docs/AGENT_SETUP.md +155 -0
- package/skills/auramaxx/docs/API.md +127 -0
- package/skills/auramaxx/docs/AUTH.md +318 -0
- package/skills/auramaxx/docs/CLI.md +130 -0
- package/skills/auramaxx/docs/MCP.md +122 -0
- package/skills/auramaxx/docs/TROUBLESHOOTING.md +357 -0
- package/skills/auramaxx/docs/WORKSPACE.md +673 -0
- package/skills/auramaxx/docs/security.md +227 -0
- package/skills/task-lifecycle/SKILL.md +378 -0
- package/src/app/api/[...doc]/page.tsx +36 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +132 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/docs/plain/route.ts +74 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +290 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +40 -0
- package/src/app/api/workspace/config/route.ts +121 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app-legacy-do-not-use/page.tsx +2245 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/approve/[actionId]/page.tsx +409 -0
- package/src/app/docs/DocsPageContent.tsx +269 -0
- package/src/app/docs/[...doc]/page.tsx +41 -0
- package/src/app/docs/page.tsx +38 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +819 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +102 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/page.tsx +1964 -0
- package/src/app/privacy/page.tsx +63 -0
- package/src/app/providers.tsx +87 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/app/terms/page.tsx +80 -0
- package/src/components/ChainSelector.tsx +44 -0
- package/src/components/HumanActionBar.tsx +697 -0
- package/src/components/NotificationDrawer.tsx +387 -0
- package/src/components/PasskeyEnrollmentPrompt.tsx +235 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +88 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +147 -0
- package/src/components/design-system/ConfirmationModal.tsx +107 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/DownloadButton.tsx +149 -0
- package/src/components/design-system/Drawer.tsx +133 -0
- package/src/components/design-system/FilterDropdown.tsx +183 -0
- package/src/components/design-system/ItemPicker.tsx +157 -0
- package/src/components/design-system/Modal.tsx +296 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +65 -0
- package/src/components/design-system/TyvekCollapsibleSection.tsx +55 -0
- package/src/components/design-system/index.ts +14 -0
- package/src/components/docs/ClientSideMarkdown.tsx +51 -0
- package/src/components/docs/DocsSearchBar.tsx +118 -0
- package/src/components/docs/DocsThemeToggle.tsx +38 -0
- package/src/components/docs/PersistentDocGroup.tsx +91 -0
- package/src/components/docs/ShareUrlButton.tsx +33 -0
- package/src/components/docs/SidebarScrollMemory.tsx +56 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/CreateViewModal.tsx +88 -0
- package/src/components/layout/LeftRail.tsx +114 -0
- package/src/components/layout/TabBar.tsx +284 -0
- package/src/components/layout/WalletSidebar.tsx +1030 -0
- package/src/components/layout/index.ts +6 -0
- package/src/components/marketing/AuraMaxxSpecOverlay.tsx +653 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1272 -0
- package/src/components/vault/AuditConsole.tsx +600 -0
- package/src/components/vault/CredentialDetail.tsx +625 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +583 -0
- package/src/components/vault/CredentialForm.tsx +1484 -0
- package/src/components/vault/CredentialList.tsx +265 -0
- package/src/components/vault/CredentialRow.tsx +130 -0
- package/src/components/vault/CredentialShareModal.tsx +273 -0
- package/src/components/vault/CredentialVault.tsx +1662 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/DocsConsole.tsx +113 -0
- package/src/components/vault/ImportCredentialsModal.tsx +578 -0
- package/src/components/vault/LargeTypeModal.tsx +88 -0
- package/src/components/vault/PasswordGenerator.tsx +232 -0
- package/src/components/vault/TOTPDisplay.tsx +108 -0
- package/src/components/vault/TotpSetupPanel.tsx +198 -0
- package/src/components/vault/VaultSidebar.tsx +881 -0
- package/src/components/vault/credentialFormName.ts +91 -0
- package/src/components/vault/hooks/useVaultKeyboardShortcuts.ts +69 -0
- package/src/components/vault/types.ts +56 -0
- package/src/context/AuthContext.tsx +365 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useAgentActions.ts +552 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/hooks/useTheme.ts +156 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api-docs.ts +154 -0
- package/src/lib/api.ts +474 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/credential-field-schema.ts +11 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +544 -0
- package/src/lib/events.ts +363 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/totp-import.ts +57 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/view-registry.ts +57 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +170 -0
- package/tailwind.config.ts +99 -0
- 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
|
+
}
|