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