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