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,1505 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Copy,
|
|
6
|
+
Edit2,
|
|
7
|
+
Save,
|
|
8
|
+
EyeOff,
|
|
9
|
+
Eye,
|
|
10
|
+
X,
|
|
11
|
+
RefreshCw,
|
|
12
|
+
Coins,
|
|
13
|
+
ArrowUpDown,
|
|
14
|
+
Search,
|
|
15
|
+
Loader2,
|
|
16
|
+
ExternalLink,
|
|
17
|
+
Wifi,
|
|
18
|
+
WifiOff,
|
|
19
|
+
Plus,
|
|
20
|
+
Trash2,
|
|
21
|
+
Lock,
|
|
22
|
+
} from 'lucide-react';
|
|
23
|
+
import { Button, ChainSelector, FilterDropdown, Popover, TextInput } from '@/components/design-system';
|
|
24
|
+
import { useWebSocket } from '@/context/WebSocketContext';
|
|
25
|
+
import { usePrice } from '@/context/PriceContext';
|
|
26
|
+
import { useAuth } from '@/context/AuthContext';
|
|
27
|
+
import { api, Api, type AssetsResponse, type TrackedAsset, type TransactionsResponse } from '@/lib/api';
|
|
28
|
+
import { useBalance } from '@/hooks/useBalance';
|
|
29
|
+
import { fetchTokenData, fetchSolanaTokenData, calculateUsdValue, formatUsdValue, type TokenData } from '@/lib/tokenData';
|
|
30
|
+
import { WALLET_EVENTS, type AssetChangedData, type BalanceUpdatedData, type TxCreatedData } from '@/lib/events';
|
|
31
|
+
|
|
32
|
+
interface WalletData {
|
|
33
|
+
address: string;
|
|
34
|
+
tier: 'cold' | 'hot' | 'temp';
|
|
35
|
+
chain: string;
|
|
36
|
+
balance?: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
color?: string;
|
|
39
|
+
emoji?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
hidden?: boolean;
|
|
42
|
+
tokenHash?: string;
|
|
43
|
+
createdAt?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
interface Transaction {
|
|
48
|
+
id: string;
|
|
49
|
+
walletAddress: string;
|
|
50
|
+
txHash: string | null;
|
|
51
|
+
type: string;
|
|
52
|
+
status: string;
|
|
53
|
+
amount: string | null;
|
|
54
|
+
tokenAddress: string | null;
|
|
55
|
+
tokenAmount: string | null;
|
|
56
|
+
from: string | null;
|
|
57
|
+
to: string | null;
|
|
58
|
+
description: string | null;
|
|
59
|
+
blockNumber: number | null;
|
|
60
|
+
chain: string;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
executedAt: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Self-contained app - only needs config with walletAddress
|
|
67
|
+
interface WalletDetailAppProps {
|
|
68
|
+
config?: {
|
|
69
|
+
walletAddress?: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const EMOJI_OPTIONS = ['🔥', '💎', '🚀', '⚡', '🌙', '🌟', '💰', '🎯', '🔮', '🌈'];
|
|
74
|
+
const COLOR_OPTIONS = ['#ff4d00', '#0047ff', '#00c853', '#ffab00', '#9c27b0', '#00bcd4', '#e91e63', '#607d8b'];
|
|
75
|
+
|
|
76
|
+
type TabType = 'assets' | 'transactions';
|
|
77
|
+
|
|
78
|
+
function formatTimeAgo(dateString: string): string {
|
|
79
|
+
const date = new Date(dateString);
|
|
80
|
+
const now = new Date();
|
|
81
|
+
const diffMs = now.getTime() - date.getTime();
|
|
82
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
83
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
84
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
85
|
+
|
|
86
|
+
if (diffMins < 1) return 'just now';
|
|
87
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
88
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
89
|
+
return `${diffDays}d ago`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function shortenAddress(address: string, chars = 6): string {
|
|
93
|
+
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isSolanaChain(chain: string): boolean {
|
|
97
|
+
return chain === 'solana' || chain === 'solana-devnet';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Chain-aware address comparison: case-insensitive for EVM hex, exact for Solana base58 */
|
|
101
|
+
function addressesMatch(a: string, b: string): boolean {
|
|
102
|
+
if (!a || !b) return false;
|
|
103
|
+
if (a.startsWith('0x') || b.startsWith('0x')) {
|
|
104
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
return a === b;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const TX_TYPE_COLORS: Record<string, string> = {
|
|
110
|
+
send: 'var(--color-warning, #ff4d00)',
|
|
111
|
+
receive: 'var(--color-success, #00c853)',
|
|
112
|
+
swap: 'var(--color-info, #0047ff)',
|
|
113
|
+
contract: 'var(--color-text-muted, #888)',
|
|
114
|
+
manual: 'var(--color-text-muted, #888)',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const TX_TYPE_OPTIONS = [
|
|
118
|
+
{ value: 'all', label: 'ALL' },
|
|
119
|
+
{ value: 'send', label: 'SEND' },
|
|
120
|
+
{ value: 'receive', label: 'RECEIVE' },
|
|
121
|
+
{ value: 'swap', label: 'SWAP' },
|
|
122
|
+
{ value: 'contract', label: 'CONTRACT' },
|
|
123
|
+
{ value: 'manual', label: 'MANUAL' },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
export const WalletDetailApp: React.FC<WalletDetailAppProps> = ({ config }) => {
|
|
129
|
+
const [manualAddress, setManualAddress] = useState('');
|
|
130
|
+
const [committedAddress, setCommittedAddress] = useState('');
|
|
131
|
+
const walletAddress = config?.walletAddress || committedAddress || undefined;
|
|
132
|
+
const { getRpcUrl, getConfiguredChains, getChainConfig, isUnlocked } = useAuth();
|
|
133
|
+
const chainOptions = Object.keys(getConfiguredChains()).map(c => ({ value: c, label: c.toUpperCase() }));
|
|
134
|
+
const { subscribe, connected } = useWebSocket();
|
|
135
|
+
const { ethPrice, formatUsd } = usePrice();
|
|
136
|
+
|
|
137
|
+
// Wallet data (fetched from API)
|
|
138
|
+
const [wallet, setWallet] = useState<WalletData | null>(null);
|
|
139
|
+
const [walletLoading, setWalletLoading] = useState(!!walletAddress);
|
|
140
|
+
const [walletError, setWalletError] = useState<string | null>(null);
|
|
141
|
+
|
|
142
|
+
// Copy state (internal)
|
|
143
|
+
const [copied, setCopied] = useState(false);
|
|
144
|
+
|
|
145
|
+
// Edit mode
|
|
146
|
+
const [isEditMode, setIsEditMode] = useState(false);
|
|
147
|
+
const [saving, setSaving] = useState(false);
|
|
148
|
+
const [activeTab, setActiveTab] = useState<TabType>('assets');
|
|
149
|
+
|
|
150
|
+
// Edit form state (initialized empty, updated when wallet loads)
|
|
151
|
+
const [editName, setEditName] = useState('');
|
|
152
|
+
const [editDescription, setEditDescription] = useState('');
|
|
153
|
+
const [editEmoji, setEditEmoji] = useState('');
|
|
154
|
+
const [editColor, setEditColor] = useState('');
|
|
155
|
+
const [editHidden, setEditHidden] = useState(false);
|
|
156
|
+
|
|
157
|
+
// Assets state
|
|
158
|
+
const [assets, setAssets] = useState<TrackedAsset[]>([]);
|
|
159
|
+
const [assetsLoading, setAssetsLoading] = useState(false);
|
|
160
|
+
const [assetsSearch, setAssetsSearch] = useState('');
|
|
161
|
+
const [tokenDataMap, setTokenDataMap] = useState<Map<string, TokenData>>(new Map());
|
|
162
|
+
const [balancesLoading, setBalancesLoading] = useState(false);
|
|
163
|
+
|
|
164
|
+
// Add asset popover state
|
|
165
|
+
const [showAddAsset, setShowAddAsset] = useState(false);
|
|
166
|
+
const [addAssetAnchor, setAddAssetAnchor] = useState<HTMLElement | null>(null);
|
|
167
|
+
const [addAssetForm, setAddAssetForm] = useState({ tokenAddress: '', symbol: '', name: '', chain: 'base' });
|
|
168
|
+
const [addingAsset, setAddingAsset] = useState(false);
|
|
169
|
+
|
|
170
|
+
// Chain filter state
|
|
171
|
+
const [selectedChain, setSelectedChain] = useState<string>('');
|
|
172
|
+
|
|
173
|
+
// Balance from RPC (via hook) — always uses the wallet's own chain, not the filter
|
|
174
|
+
const { balance, loading: balanceLoading, currency } = useBalance(wallet?.address, wallet?.chain);
|
|
175
|
+
|
|
176
|
+
// Transactions state
|
|
177
|
+
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
|
178
|
+
const [txLoading, setTxLoading] = useState(false);
|
|
179
|
+
const [txSearch, setTxSearch] = useState('');
|
|
180
|
+
const [txTypeFilter, setTxTypeFilter] = useState('all');
|
|
181
|
+
const [txHasMore, setTxHasMore] = useState(false);
|
|
182
|
+
const [txOffset, setTxOffset] = useState(0);
|
|
183
|
+
|
|
184
|
+
// Fetch wallet data from API
|
|
185
|
+
const fetchWallet = useCallback(async () => {
|
|
186
|
+
if (!walletAddress || !isUnlocked) return;
|
|
187
|
+
|
|
188
|
+
setWalletLoading(true);
|
|
189
|
+
setWalletError(null);
|
|
190
|
+
try {
|
|
191
|
+
const data = await Promise.race([
|
|
192
|
+
api.get<WalletData>(Api.Wallet, `/wallet/${walletAddress}`),
|
|
193
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Server unreachable')), 5000)),
|
|
194
|
+
]);
|
|
195
|
+
setWallet(data);
|
|
196
|
+
// Initialize edit form with fetched data
|
|
197
|
+
setEditName(data.name || '');
|
|
198
|
+
setEditDescription(data.description || '');
|
|
199
|
+
setEditEmoji(data.emoji || '');
|
|
200
|
+
setEditColor(data.color || '');
|
|
201
|
+
setEditHidden(data.hidden || false);
|
|
202
|
+
setSelectedChain(data.chain || 'base');
|
|
203
|
+
setAddAssetForm(prev => ({ ...prev, chain: data.chain || 'base' }));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error('[WalletDetail] Failed to fetch wallet:', err);
|
|
206
|
+
setWalletError(err instanceof Error ? err.message : 'Failed to fetch wallet');
|
|
207
|
+
} finally {
|
|
208
|
+
setWalletLoading(false);
|
|
209
|
+
}
|
|
210
|
+
}, [walletAddress, isUnlocked]);
|
|
211
|
+
|
|
212
|
+
// Copy address to clipboard
|
|
213
|
+
const copyAddress = useCallback(() => {
|
|
214
|
+
if (wallet?.address) {
|
|
215
|
+
navigator.clipboard.writeText(wallet.address);
|
|
216
|
+
setCopied(true);
|
|
217
|
+
setTimeout(() => setCopied(false), 2000);
|
|
218
|
+
}
|
|
219
|
+
}, [wallet?.address]);
|
|
220
|
+
|
|
221
|
+
// Fetch wallet on mount
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
fetchWallet();
|
|
224
|
+
}, [fetchWallet]);
|
|
225
|
+
|
|
226
|
+
// Fetch assets from backend API
|
|
227
|
+
const fetchAssets = useCallback(async () => {
|
|
228
|
+
if (!wallet || !selectedChain) return;
|
|
229
|
+
setAssetsLoading(true);
|
|
230
|
+
try {
|
|
231
|
+
const data = await api.get<AssetsResponse>(Api.Wallet, `/wallet/${wallet.address}/assets`, {
|
|
232
|
+
sortBy: 'updatedAt',
|
|
233
|
+
sortDir: 'desc',
|
|
234
|
+
limit: 100,
|
|
235
|
+
chain: selectedChain,
|
|
236
|
+
});
|
|
237
|
+
if (data.success) {
|
|
238
|
+
setAssets(data.assets);
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error('[WalletDetail] Failed to fetch assets:', err);
|
|
242
|
+
} finally {
|
|
243
|
+
setAssetsLoading(false);
|
|
244
|
+
}
|
|
245
|
+
}, [wallet, selectedChain]);
|
|
246
|
+
|
|
247
|
+
// Fetch token balances (EVM uses batch eth_call, Solana uses getParsedTokenAccountsByOwner)
|
|
248
|
+
const fetchBalances = useCallback(async (assetList: TrackedAsset[]) => {
|
|
249
|
+
if (assetList.length === 0 || !wallet || !selectedChain) return;
|
|
250
|
+
|
|
251
|
+
setBalancesLoading(true);
|
|
252
|
+
try {
|
|
253
|
+
const assetInfos = assetList.map(a => ({
|
|
254
|
+
tokenAddress: a.tokenAddress,
|
|
255
|
+
decimals: a.decimals,
|
|
256
|
+
poolAddress: a.poolAddress,
|
|
257
|
+
poolVersion: a.poolVersion,
|
|
258
|
+
}));
|
|
259
|
+
|
|
260
|
+
const rpcUrl = getRpcUrl(selectedChain);
|
|
261
|
+
const data = isSolanaChain(selectedChain)
|
|
262
|
+
? await fetchSolanaTokenData(wallet.address, assetInfos, rpcUrl)
|
|
263
|
+
: await fetchTokenData(wallet.address, assetInfos, rpcUrl);
|
|
264
|
+
setTokenDataMap(data);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error('[WalletDetail] Failed to fetch balances:', err);
|
|
267
|
+
} finally {
|
|
268
|
+
setBalancesLoading(false);
|
|
269
|
+
}
|
|
270
|
+
}, [wallet, selectedChain, getRpcUrl]);
|
|
271
|
+
|
|
272
|
+
// Fetch transactions from backend API
|
|
273
|
+
const fetchTransactions = useCallback(async (reset = false) => {
|
|
274
|
+
if (!wallet || !selectedChain) return;
|
|
275
|
+
setTxLoading(true);
|
|
276
|
+
try {
|
|
277
|
+
const offset = reset ? 0 : txOffset;
|
|
278
|
+
const params: Record<string, string | number> = {
|
|
279
|
+
limit: 50,
|
|
280
|
+
offset,
|
|
281
|
+
sortBy: 'createdAt',
|
|
282
|
+
sortDir: 'desc',
|
|
283
|
+
chain: selectedChain,
|
|
284
|
+
};
|
|
285
|
+
if (txTypeFilter !== 'all') {
|
|
286
|
+
params.type = txTypeFilter;
|
|
287
|
+
}
|
|
288
|
+
if (txSearch) {
|
|
289
|
+
params.search = txSearch;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const data = await api.get<TransactionsResponse>(Api.Wallet, `/wallet/${wallet.address}/transactions`, params);
|
|
293
|
+
if (data.success) {
|
|
294
|
+
if (reset) {
|
|
295
|
+
setTransactions(data.transactions);
|
|
296
|
+
setTxOffset(data.transactions.length);
|
|
297
|
+
} else {
|
|
298
|
+
setTransactions(prev => [...prev, ...data.transactions]);
|
|
299
|
+
setTxOffset(offset + data.transactions.length);
|
|
300
|
+
}
|
|
301
|
+
setTxHasMore(data.pagination.hasMore);
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error('[WalletDetail] Failed to fetch transactions:', err);
|
|
305
|
+
} finally {
|
|
306
|
+
setTxLoading(false);
|
|
307
|
+
}
|
|
308
|
+
}, [wallet, selectedChain, txTypeFilter, txSearch, txOffset]);
|
|
309
|
+
|
|
310
|
+
// Refetch all data when selectedChain changes (also handles initial load)
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (wallet && selectedChain) {
|
|
313
|
+
fetchAssets();
|
|
314
|
+
fetchTransactions(true);
|
|
315
|
+
}
|
|
316
|
+
}, [selectedChain]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
317
|
+
|
|
318
|
+
// Fetch balances when assets change
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (assets.length > 0) {
|
|
321
|
+
fetchBalances(assets);
|
|
322
|
+
}
|
|
323
|
+
}, [assets, fetchBalances]);
|
|
324
|
+
|
|
325
|
+
// Refetch transactions when filter changes
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
fetchTransactions(true);
|
|
328
|
+
}, [txTypeFilter]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
329
|
+
|
|
330
|
+
// WebSocket subscriptions
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (!wallet) return;
|
|
333
|
+
|
|
334
|
+
const unsubscribeAsset = subscribe('asset:changed', (event) => {
|
|
335
|
+
const data = event.data as AssetChangedData;
|
|
336
|
+
if (!addressesMatch(data.walletAddress, wallet.address)) return;
|
|
337
|
+
|
|
338
|
+
// Handle removal
|
|
339
|
+
if (data.removed) {
|
|
340
|
+
setAssets(prev => prev.filter(
|
|
341
|
+
a => !addressesMatch(a.tokenAddress, data.tokenAddress)
|
|
342
|
+
));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Update or add asset in local state
|
|
347
|
+
setAssets(prev => {
|
|
348
|
+
const existingIndex = prev.findIndex(
|
|
349
|
+
a => addressesMatch(a.tokenAddress, data.tokenAddress)
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
if (existingIndex >= 0) {
|
|
353
|
+
// Update existing
|
|
354
|
+
const updated = [...prev];
|
|
355
|
+
updated[existingIndex] = {
|
|
356
|
+
...updated[existingIndex],
|
|
357
|
+
symbol: data.symbol ?? updated[existingIndex].symbol,
|
|
358
|
+
name: data.name ?? updated[existingIndex].name,
|
|
359
|
+
poolAddress: data.poolAddress ?? updated[existingIndex].poolAddress,
|
|
360
|
+
poolVersion: data.poolVersion ?? updated[existingIndex].poolVersion,
|
|
361
|
+
icon: data.icon ?? updated[existingIndex].icon,
|
|
362
|
+
updatedAt: new Date().toISOString(),
|
|
363
|
+
};
|
|
364
|
+
// Re-sort by updatedAt
|
|
365
|
+
return updated.sort((a, b) =>
|
|
366
|
+
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
// Add new (will need to refetch to get full data)
|
|
370
|
+
fetchAssets();
|
|
371
|
+
return prev;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Subscribe to balance:updated events from cron server
|
|
377
|
+
const unsubscribeBalance = subscribe(WALLET_EVENTS.BALANCE_UPDATED, (event) => {
|
|
378
|
+
const data = event.data as BalanceUpdatedData;
|
|
379
|
+
|
|
380
|
+
if (data.type === 'token') {
|
|
381
|
+
// Update token balances in the assets list
|
|
382
|
+
setAssets(prev => {
|
|
383
|
+
let changed = false;
|
|
384
|
+
const updated = prev.map(asset => {
|
|
385
|
+
const match = data.balances.find(b =>
|
|
386
|
+
addressesMatch(b.walletAddress, wallet.address) &&
|
|
387
|
+
b.tokenAddress && addressesMatch(b.tokenAddress, asset.tokenAddress)
|
|
388
|
+
);
|
|
389
|
+
if (match) {
|
|
390
|
+
changed = true;
|
|
391
|
+
return { ...asset, lastBalance: match.balance, lastBalanceAt: new Date().toISOString() };
|
|
392
|
+
}
|
|
393
|
+
return asset;
|
|
394
|
+
});
|
|
395
|
+
return changed ? updated : prev;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const unsubscribeTx = subscribe('tx:created', (event) => {
|
|
401
|
+
const data = event.data as TxCreatedData;
|
|
402
|
+
if (!addressesMatch(data.walletAddress, wallet.address)) return;
|
|
403
|
+
|
|
404
|
+
// Prepend new transaction to local state
|
|
405
|
+
const newTx: Transaction = {
|
|
406
|
+
id: data.id,
|
|
407
|
+
walletAddress: data.walletAddress,
|
|
408
|
+
txHash: data.txHash ?? null,
|
|
409
|
+
type: data.type,
|
|
410
|
+
status: 'confirmed',
|
|
411
|
+
amount: data.amount ?? null,
|
|
412
|
+
tokenAddress: data.tokenAddress ?? null,
|
|
413
|
+
tokenAmount: data.tokenAmount ?? null,
|
|
414
|
+
from: null,
|
|
415
|
+
to: null,
|
|
416
|
+
description: data.description ?? null,
|
|
417
|
+
blockNumber: null,
|
|
418
|
+
chain: wallet.chain || 'base',
|
|
419
|
+
createdAt: new Date().toISOString(),
|
|
420
|
+
updatedAt: new Date().toISOString(),
|
|
421
|
+
executedAt: new Date().toISOString(),
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
setTransactions(prev => [newTx, ...prev]);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return () => {
|
|
428
|
+
unsubscribeAsset();
|
|
429
|
+
unsubscribeBalance();
|
|
430
|
+
unsubscribeTx();
|
|
431
|
+
};
|
|
432
|
+
}, [subscribe, wallet, fetchAssets]);
|
|
433
|
+
|
|
434
|
+
const handleSave = async () => {
|
|
435
|
+
if (!wallet) return;
|
|
436
|
+
setSaving(true);
|
|
437
|
+
try {
|
|
438
|
+
await api.post(Api.Wallet, '/wallet/rename', {
|
|
439
|
+
address: wallet.address,
|
|
440
|
+
name: editName || undefined,
|
|
441
|
+
description: editDescription || undefined,
|
|
442
|
+
emoji: editEmoji || undefined,
|
|
443
|
+
color: editColor || undefined,
|
|
444
|
+
hidden: editHidden,
|
|
445
|
+
});
|
|
446
|
+
// Update local wallet state
|
|
447
|
+
setWallet(prev => prev ? {
|
|
448
|
+
...prev,
|
|
449
|
+
name: editName || undefined,
|
|
450
|
+
description: editDescription || undefined,
|
|
451
|
+
emoji: editEmoji || undefined,
|
|
452
|
+
color: editColor || undefined,
|
|
453
|
+
hidden: editHidden,
|
|
454
|
+
} : null);
|
|
455
|
+
setIsEditMode(false);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.error('[WalletDetail] Failed to save:', err);
|
|
458
|
+
} finally {
|
|
459
|
+
setSaving(false);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const handleCancel = () => {
|
|
464
|
+
if (!wallet) return;
|
|
465
|
+
setEditName(wallet.name || '');
|
|
466
|
+
setEditDescription(wallet.description || '');
|
|
467
|
+
setEditEmoji(wallet.emoji || '');
|
|
468
|
+
setEditColor(wallet.color || '');
|
|
469
|
+
setEditHidden(wallet.hidden || false);
|
|
470
|
+
setIsEditMode(false);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const handleRefreshAssets = () => {
|
|
474
|
+
fetchAssets();
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const handleRefreshTx = () => {
|
|
478
|
+
fetchTransactions(true);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const handleChainChange = (chain: string) => {
|
|
482
|
+
setSelectedChain(chain);
|
|
483
|
+
setAddAssetForm(prev => ({ ...prev, chain }));
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// Add asset handler
|
|
487
|
+
const handleAddAsset = async () => {
|
|
488
|
+
if (!addAssetForm.tokenAddress || !wallet) return;
|
|
489
|
+
|
|
490
|
+
setAddingAsset(true);
|
|
491
|
+
try {
|
|
492
|
+
const data = await api.post<{ success: boolean; asset?: TrackedAsset; error?: string }>(
|
|
493
|
+
Api.Wallet,
|
|
494
|
+
`/wallet/${wallet.address}/asset`,
|
|
495
|
+
{
|
|
496
|
+
tokenAddress: addAssetForm.tokenAddress,
|
|
497
|
+
symbol: addAssetForm.symbol || undefined,
|
|
498
|
+
name: addAssetForm.name || undefined,
|
|
499
|
+
chain: addAssetForm.chain,
|
|
500
|
+
}
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (data.success) {
|
|
504
|
+
setShowAddAsset(false);
|
|
505
|
+
setAddAssetForm({ tokenAddress: '', symbol: '', name: '', chain: wallet.chain || 'base' });
|
|
506
|
+
fetchAssets(); // Refresh the list
|
|
507
|
+
} else {
|
|
508
|
+
console.error('[WalletDetail] Failed to add asset:', data.error);
|
|
509
|
+
}
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.error('[WalletDetail] Failed to add asset:', err);
|
|
512
|
+
} finally {
|
|
513
|
+
setAddingAsset(false);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Remove asset handler
|
|
518
|
+
const handleRemoveAsset = async (assetId: string) => {
|
|
519
|
+
if (!wallet) return;
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const data = await api.delete<{ success: boolean; error?: string }>(
|
|
523
|
+
Api.Wallet,
|
|
524
|
+
`/wallet/${wallet.address}/asset/${assetId}`
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
if (!data.success) {
|
|
528
|
+
console.error('[WalletDetail] Failed to remove asset:', data.error);
|
|
529
|
+
}
|
|
530
|
+
// WebSocket event will update the UI
|
|
531
|
+
} catch (err) {
|
|
532
|
+
console.error('[WalletDetail] Failed to remove asset:', err);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// Filter assets by search
|
|
537
|
+
const filteredAssets = assets.filter(a => {
|
|
538
|
+
// Search filter
|
|
539
|
+
if (assetsSearch) {
|
|
540
|
+
const search = assetsSearch.toLowerCase();
|
|
541
|
+
return (
|
|
542
|
+
(a.symbol?.toLowerCase().includes(search)) ||
|
|
543
|
+
(a.name?.toLowerCase().includes(search)) ||
|
|
544
|
+
(a.tokenAddress.toLowerCase().includes(search))
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return true;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Locked state
|
|
551
|
+
if (!isUnlocked) {
|
|
552
|
+
return (
|
|
553
|
+
<div className="py-8 text-center">
|
|
554
|
+
<Lock
|
|
555
|
+
size={24}
|
|
556
|
+
className="mx-auto mb-3"
|
|
557
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
558
|
+
/>
|
|
559
|
+
<div
|
|
560
|
+
className="font-mono text-[10px] tracking-wider"
|
|
561
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
562
|
+
>
|
|
563
|
+
VAULT LOCKED
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// No address configured - show input
|
|
570
|
+
if (!walletAddress) {
|
|
571
|
+
return (
|
|
572
|
+
<div className="space-y-3 py-4 px-1">
|
|
573
|
+
<div className="text-center">
|
|
574
|
+
<Search
|
|
575
|
+
size={20}
|
|
576
|
+
className="mx-auto mb-2"
|
|
577
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
578
|
+
/>
|
|
579
|
+
<div
|
|
580
|
+
className="font-mono text-[10px] tracking-wider"
|
|
581
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
582
|
+
>
|
|
583
|
+
ENTER WALLET ADDRESS
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
<div className="space-y-2">
|
|
587
|
+
<TextInput
|
|
588
|
+
value={manualAddress}
|
|
589
|
+
onChange={(e) => setManualAddress(e.target.value)}
|
|
590
|
+
placeholder="0x... or base58 address"
|
|
591
|
+
compact
|
|
592
|
+
onKeyDown={(e) => {
|
|
593
|
+
if (e.key === 'Enter' && manualAddress.trim()) {
|
|
594
|
+
setCommittedAddress(manualAddress.trim());
|
|
595
|
+
setWalletLoading(true);
|
|
596
|
+
}
|
|
597
|
+
}}
|
|
598
|
+
/>
|
|
599
|
+
<Button
|
|
600
|
+
variant="primary"
|
|
601
|
+
onClick={() => {
|
|
602
|
+
if (manualAddress.trim()) {
|
|
603
|
+
setCommittedAddress(manualAddress.trim());
|
|
604
|
+
setWalletLoading(true);
|
|
605
|
+
}
|
|
606
|
+
}}
|
|
607
|
+
disabled={!manualAddress.trim()}
|
|
608
|
+
className="w-full"
|
|
609
|
+
size="sm"
|
|
610
|
+
>
|
|
611
|
+
LOAD WALLET
|
|
612
|
+
</Button>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Loading state
|
|
619
|
+
if (walletLoading) {
|
|
620
|
+
return (
|
|
621
|
+
<div className="py-8 text-center">
|
|
622
|
+
<Loader2
|
|
623
|
+
size={24}
|
|
624
|
+
className="mx-auto mb-3 animate-spin"
|
|
625
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
626
|
+
/>
|
|
627
|
+
<div
|
|
628
|
+
className="font-mono text-[10px] tracking-wider"
|
|
629
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
630
|
+
>
|
|
631
|
+
LOADING WALLET...
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Error state
|
|
638
|
+
if (walletError || !wallet) {
|
|
639
|
+
return (
|
|
640
|
+
<div className="py-8 text-center">
|
|
641
|
+
<div
|
|
642
|
+
className="font-mono text-[10px] tracking-wider"
|
|
643
|
+
style={{ color: 'var(--color-warning, #ff4d00)' }}
|
|
644
|
+
>
|
|
645
|
+
{walletError || 'WALLET NOT FOUND'}
|
|
646
|
+
</div>
|
|
647
|
+
<div
|
|
648
|
+
className="font-mono text-[8px] mt-1"
|
|
649
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
650
|
+
>
|
|
651
|
+
{walletAddress}
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return (
|
|
658
|
+
<div className="space-y-2">
|
|
659
|
+
{/* Balance - Prominent at top */}
|
|
660
|
+
<div className="text-center py-2">
|
|
661
|
+
<div
|
|
662
|
+
className="font-mono text-2xl font-bold"
|
|
663
|
+
style={{ color: 'var(--color-text, #0a0a0a)' }}
|
|
664
|
+
>
|
|
665
|
+
{balanceLoading ? (
|
|
666
|
+
<Loader2 size={20} className="inline animate-spin" style={{ color: 'var(--color-text-muted, #888)' }} />
|
|
667
|
+
) : (
|
|
668
|
+
<>{balance || '0'} <span className="text-sm">{currency}</span></>
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
{ethPrice && balance && !balanceLoading && !isSolanaChain(wallet.chain) && (
|
|
672
|
+
<div className="font-mono text-sm" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
673
|
+
{formatUsd(balance)}
|
|
674
|
+
</div>
|
|
675
|
+
)}
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
{/* Compact Header: Name, Address, Tier */}
|
|
679
|
+
<div className="flex items-center gap-2">
|
|
680
|
+
{wallet.emoji && <span className="text-xs">{wallet.emoji}</span>}
|
|
681
|
+
<span
|
|
682
|
+
className="font-mono text-[9px] font-bold truncate"
|
|
683
|
+
style={{ color: 'var(--color-text, #0a0a0a)' }}
|
|
684
|
+
>
|
|
685
|
+
{wallet.name || 'HOT WALLET'}
|
|
686
|
+
</span>
|
|
687
|
+
{wallet.color && (
|
|
688
|
+
<div className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: wallet.color }} />
|
|
689
|
+
)}
|
|
690
|
+
<span
|
|
691
|
+
className="font-mono text-[8px] uppercase px-1 py-0.5"
|
|
692
|
+
style={{
|
|
693
|
+
background: 'var(--color-warning, #ff4d00)',
|
|
694
|
+
color: 'var(--color-surface, #fff)',
|
|
695
|
+
}}
|
|
696
|
+
>
|
|
697
|
+
{wallet.tier}
|
|
698
|
+
</span>
|
|
699
|
+
<div className="ml-auto flex items-center gap-1">
|
|
700
|
+
{connected ? (
|
|
701
|
+
<Wifi size={9} style={{ color: 'var(--color-success, #00c853)' }} />
|
|
702
|
+
) : (
|
|
703
|
+
<WifiOff size={9} style={{ color: 'var(--color-text-muted, #888)' }} />
|
|
704
|
+
)}
|
|
705
|
+
<button
|
|
706
|
+
onClick={() => setIsEditMode(true)}
|
|
707
|
+
className="p-1 transition-colors"
|
|
708
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
709
|
+
>
|
|
710
|
+
<Edit2 size={10} />
|
|
711
|
+
</button>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
|
|
715
|
+
{/* Compact Address with Copy */}
|
|
716
|
+
<div className="flex items-center gap-1.5">
|
|
717
|
+
<code
|
|
718
|
+
className="flex-1 font-mono text-[9px] truncate select-all"
|
|
719
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
720
|
+
>
|
|
721
|
+
{wallet.address}
|
|
722
|
+
</code>
|
|
723
|
+
<button
|
|
724
|
+
onClick={copyAddress}
|
|
725
|
+
className="p-1 transition-colors shrink-0"
|
|
726
|
+
style={{
|
|
727
|
+
background: copied ? 'var(--color-success, #00c853)' : 'var(--color-background-alt, #f5f5f5)',
|
|
728
|
+
color: copied ? 'var(--color-surface, #fff)' : 'var(--color-text-muted, #888)',
|
|
729
|
+
}}
|
|
730
|
+
>
|
|
731
|
+
<Copy size={10} />
|
|
732
|
+
</button>
|
|
733
|
+
</div>
|
|
734
|
+
|
|
735
|
+
{/* Hidden Badge */}
|
|
736
|
+
{wallet.hidden && !isEditMode && (
|
|
737
|
+
<div
|
|
738
|
+
className="flex items-center gap-1.5 px-2 py-1"
|
|
739
|
+
style={{
|
|
740
|
+
background: 'color-mix(in srgb, var(--color-warning, #ff4d00) 10%, transparent)',
|
|
741
|
+
border: '1px solid color-mix(in srgb, var(--color-warning, #ff4d00) 30%, transparent)',
|
|
742
|
+
}}
|
|
743
|
+
>
|
|
744
|
+
<EyeOff size={9} style={{ color: 'var(--color-warning, #ff4d00)' }} />
|
|
745
|
+
<span className="font-mono text-[8px]" style={{ color: 'var(--color-warning, #ff4d00)' }}>
|
|
746
|
+
HIDDEN - Excluded from totals
|
|
747
|
+
</span>
|
|
748
|
+
</div>
|
|
749
|
+
)}
|
|
750
|
+
|
|
751
|
+
{/* Description */}
|
|
752
|
+
{wallet.description && !isEditMode && (
|
|
753
|
+
<div
|
|
754
|
+
className="font-mono text-[9px] leading-relaxed"
|
|
755
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
756
|
+
>
|
|
757
|
+
{wallet.description}
|
|
758
|
+
</div>
|
|
759
|
+
)}
|
|
760
|
+
|
|
761
|
+
{/* Edit Mode */}
|
|
762
|
+
{isEditMode ? (
|
|
763
|
+
<div
|
|
764
|
+
className="space-y-2 pt-2"
|
|
765
|
+
style={{ borderTop: '1px solid var(--color-border, #e5e5e5)' }}
|
|
766
|
+
>
|
|
767
|
+
<div>
|
|
768
|
+
<label
|
|
769
|
+
className="font-mono text-[7px] tracking-widest block mb-0.5"
|
|
770
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
771
|
+
>
|
|
772
|
+
NAME
|
|
773
|
+
</label>
|
|
774
|
+
<input
|
|
775
|
+
type="text"
|
|
776
|
+
value={editName}
|
|
777
|
+
onChange={(e) => setEditName(e.target.value)}
|
|
778
|
+
placeholder="Wallet name..."
|
|
779
|
+
className="w-full px-2 py-1.5 font-mono text-[10px] focus:outline-none"
|
|
780
|
+
style={{
|
|
781
|
+
background: 'var(--color-surface, #fff)',
|
|
782
|
+
border: '1px solid var(--color-border, #e5e5e5)',
|
|
783
|
+
color: 'var(--color-text, #0a0a0a)',
|
|
784
|
+
}}
|
|
785
|
+
/>
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
<div>
|
|
789
|
+
<label
|
|
790
|
+
className="font-mono text-[7px] tracking-widest block mb-0.5"
|
|
791
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
792
|
+
>
|
|
793
|
+
DESCRIPTION
|
|
794
|
+
</label>
|
|
795
|
+
<textarea
|
|
796
|
+
value={editDescription}
|
|
797
|
+
onChange={(e) => setEditDescription(e.target.value)}
|
|
798
|
+
placeholder="Add a description..."
|
|
799
|
+
rows={2}
|
|
800
|
+
className="w-full px-2 py-1.5 font-mono text-[10px] focus:outline-none resize-none"
|
|
801
|
+
style={{
|
|
802
|
+
background: 'var(--color-surface, #fff)',
|
|
803
|
+
border: '1px solid var(--color-border, #e5e5e5)',
|
|
804
|
+
color: 'var(--color-text, #0a0a0a)',
|
|
805
|
+
}}
|
|
806
|
+
/>
|
|
807
|
+
</div>
|
|
808
|
+
|
|
809
|
+
<div>
|
|
810
|
+
<label
|
|
811
|
+
className="font-mono text-[7px] tracking-widest block mb-0.5"
|
|
812
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
813
|
+
>
|
|
814
|
+
EMOJI
|
|
815
|
+
</label>
|
|
816
|
+
<div className="flex flex-wrap gap-0.5">
|
|
817
|
+
<button
|
|
818
|
+
onClick={() => setEditEmoji('')}
|
|
819
|
+
className="w-6 h-6 flex items-center justify-center transition-colors"
|
|
820
|
+
style={{
|
|
821
|
+
border: !editEmoji
|
|
822
|
+
? '1px solid var(--color-text, #0a0a0a)'
|
|
823
|
+
: '1px solid var(--color-border, #e5e5e5)',
|
|
824
|
+
background: !editEmoji ? 'var(--color-background-alt, #f5f5f5)' : 'transparent',
|
|
825
|
+
}}
|
|
826
|
+
>
|
|
827
|
+
<X size={10} style={{ color: 'var(--color-text-muted, #888)' }} />
|
|
828
|
+
</button>
|
|
829
|
+
{EMOJI_OPTIONS.map((emoji) => (
|
|
830
|
+
<button
|
|
831
|
+
key={emoji}
|
|
832
|
+
onClick={() => setEditEmoji(emoji)}
|
|
833
|
+
className="w-6 h-6 flex items-center justify-center text-xs transition-colors"
|
|
834
|
+
style={{
|
|
835
|
+
border: editEmoji === emoji
|
|
836
|
+
? '1px solid var(--color-text, #0a0a0a)'
|
|
837
|
+
: '1px solid var(--color-border, #e5e5e5)',
|
|
838
|
+
background: editEmoji === emoji ? 'var(--color-background-alt, #f5f5f5)' : 'transparent',
|
|
839
|
+
}}
|
|
840
|
+
>
|
|
841
|
+
{emoji}
|
|
842
|
+
</button>
|
|
843
|
+
))}
|
|
844
|
+
</div>
|
|
845
|
+
</div>
|
|
846
|
+
|
|
847
|
+
<div>
|
|
848
|
+
<label
|
|
849
|
+
className="font-mono text-[7px] tracking-widest block mb-0.5"
|
|
850
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
851
|
+
>
|
|
852
|
+
COLOR
|
|
853
|
+
</label>
|
|
854
|
+
<div className="flex flex-wrap gap-0.5">
|
|
855
|
+
<button
|
|
856
|
+
onClick={() => setEditColor('')}
|
|
857
|
+
className="w-6 h-6 flex items-center justify-center transition-colors"
|
|
858
|
+
style={{
|
|
859
|
+
border: !editColor
|
|
860
|
+
? '1px solid var(--color-text, #0a0a0a)'
|
|
861
|
+
: '1px solid var(--color-border, #e5e5e5)',
|
|
862
|
+
}}
|
|
863
|
+
>
|
|
864
|
+
<div
|
|
865
|
+
className="w-3 h-3 rounded-full"
|
|
866
|
+
style={{ background: 'var(--color-border, #e5e5e5)' }}
|
|
867
|
+
/>
|
|
868
|
+
</button>
|
|
869
|
+
{COLOR_OPTIONS.map((color) => (
|
|
870
|
+
<button
|
|
871
|
+
key={color}
|
|
872
|
+
onClick={() => setEditColor(color)}
|
|
873
|
+
className="w-6 h-6 flex items-center justify-center transition-colors"
|
|
874
|
+
style={{
|
|
875
|
+
border: editColor === color
|
|
876
|
+
? '1px solid var(--color-text, #0a0a0a)'
|
|
877
|
+
: '1px solid var(--color-border, #e5e5e5)',
|
|
878
|
+
}}
|
|
879
|
+
>
|
|
880
|
+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: color }} />
|
|
881
|
+
</button>
|
|
882
|
+
))}
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
<div>
|
|
887
|
+
<label
|
|
888
|
+
className="font-mono text-[7px] tracking-widest block mb-0.5"
|
|
889
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
890
|
+
>
|
|
891
|
+
VISIBILITY
|
|
892
|
+
</label>
|
|
893
|
+
<button
|
|
894
|
+
onClick={() => setEditHidden(!editHidden)}
|
|
895
|
+
className="flex items-center gap-1.5 px-2 py-1.5 w-full transition-colors"
|
|
896
|
+
style={{
|
|
897
|
+
border: editHidden
|
|
898
|
+
? '1px solid var(--color-warning, #ff4d00)'
|
|
899
|
+
: '1px solid var(--color-border, #e5e5e5)',
|
|
900
|
+
background: editHidden
|
|
901
|
+
? 'color-mix(in srgb, var(--color-warning, #ff4d00) 5%, transparent)'
|
|
902
|
+
: 'transparent',
|
|
903
|
+
color: editHidden ? 'var(--color-warning, #ff4d00)' : 'var(--color-text, #0a0a0a)',
|
|
904
|
+
}}
|
|
905
|
+
>
|
|
906
|
+
{editHidden ? <EyeOff size={10} /> : <Eye size={10} />}
|
|
907
|
+
<span className="font-mono text-[9px]">
|
|
908
|
+
{editHidden ? 'HIDDEN' : 'VISIBLE'}
|
|
909
|
+
</span>
|
|
910
|
+
</button>
|
|
911
|
+
</div>
|
|
912
|
+
|
|
913
|
+
<div className="flex gap-1.5 pt-1">
|
|
914
|
+
<Button variant="secondary" onClick={handleCancel} className="flex-1">
|
|
915
|
+
CANCEL
|
|
916
|
+
</Button>
|
|
917
|
+
<Button
|
|
918
|
+
variant="primary"
|
|
919
|
+
onClick={handleSave}
|
|
920
|
+
loading={saving}
|
|
921
|
+
icon={<Save size={9} />}
|
|
922
|
+
className="flex-1"
|
|
923
|
+
>
|
|
924
|
+
SAVE
|
|
925
|
+
</Button>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
) : (
|
|
929
|
+
<>
|
|
930
|
+
{/* Tab Navigation */}
|
|
931
|
+
<div className="flex items-center gap-2">
|
|
932
|
+
<div
|
|
933
|
+
className="flex flex-1 rounded-sm overflow-hidden"
|
|
934
|
+
style={{
|
|
935
|
+
background: 'var(--color-background-alt, #f5f5f5)',
|
|
936
|
+
border: '1px solid var(--color-border, #e5e5e5)',
|
|
937
|
+
}}
|
|
938
|
+
>
|
|
939
|
+
<TabButton
|
|
940
|
+
active={activeTab === 'assets'}
|
|
941
|
+
onClick={() => setActiveTab('assets')}
|
|
942
|
+
icon={<Coins size={12} />}
|
|
943
|
+
label="ASSETS"
|
|
944
|
+
badge={assets.length > 0 ? assets.length : undefined}
|
|
945
|
+
/>
|
|
946
|
+
<TabButton
|
|
947
|
+
active={activeTab === 'transactions'}
|
|
948
|
+
onClick={() => setActiveTab('transactions')}
|
|
949
|
+
icon={<ArrowUpDown size={12} />}
|
|
950
|
+
label="TRANSACTIONS"
|
|
951
|
+
badge={transactions.length > 0 ? transactions.length : undefined}
|
|
952
|
+
/>
|
|
953
|
+
</div>
|
|
954
|
+
<Button
|
|
955
|
+
variant="secondary"
|
|
956
|
+
size="sm"
|
|
957
|
+
onClick={activeTab === 'assets' ? handleRefreshAssets : handleRefreshTx}
|
|
958
|
+
disabled={activeTab === 'assets' ? (assetsLoading || balancesLoading) : txLoading}
|
|
959
|
+
icon={<RefreshCw size={12} className={(activeTab === 'assets' ? (assetsLoading || balancesLoading) : txLoading) ? 'animate-spin' : ''} />}
|
|
960
|
+
/>
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
{/* Tab Content */}
|
|
964
|
+
{activeTab === 'assets' ? (
|
|
965
|
+
<AssetsTab
|
|
966
|
+
assets={filteredAssets}
|
|
967
|
+
loading={assetsLoading || balancesLoading}
|
|
968
|
+
search={assetsSearch}
|
|
969
|
+
onSearchChange={setAssetsSearch}
|
|
970
|
+
tokenDataMap={tokenDataMap}
|
|
971
|
+
ethPrice={ethPrice}
|
|
972
|
+
showAddAsset={showAddAsset}
|
|
973
|
+
addAssetAnchor={addAssetAnchor}
|
|
974
|
+
onShowAddAsset={(show, anchor) => {
|
|
975
|
+
setShowAddAsset(show);
|
|
976
|
+
setAddAssetAnchor(anchor || null);
|
|
977
|
+
}}
|
|
978
|
+
addAssetForm={addAssetForm}
|
|
979
|
+
onAddAssetFormChange={setAddAssetForm}
|
|
980
|
+
onAddAsset={handleAddAsset}
|
|
981
|
+
addingAsset={addingAsset}
|
|
982
|
+
chainOptions={chainOptions}
|
|
983
|
+
selectedChain={selectedChain}
|
|
984
|
+
onChainChange={handleChainChange}
|
|
985
|
+
onRemoveAsset={handleRemoveAsset}
|
|
986
|
+
nativeCurrency={isSolanaChain(selectedChain) ? 'SOL' : 'ETH'}
|
|
987
|
+
/>
|
|
988
|
+
) : (
|
|
989
|
+
<TransactionsTab
|
|
990
|
+
transactions={transactions}
|
|
991
|
+
loading={txLoading}
|
|
992
|
+
search={txSearch}
|
|
993
|
+
onSearchChange={setTxSearch}
|
|
994
|
+
typeFilter={txTypeFilter}
|
|
995
|
+
onTypeFilterChange={setTxTypeFilter}
|
|
996
|
+
hasMore={txHasMore}
|
|
997
|
+
onLoadMore={() => fetchTransactions(false)}
|
|
998
|
+
nativeCurrency={isSolanaChain(selectedChain) ? 'SOL' : 'ETH'}
|
|
999
|
+
explorerUrl={getChainConfig(selectedChain).explorer}
|
|
1000
|
+
/>
|
|
1001
|
+
)}
|
|
1002
|
+
</>
|
|
1003
|
+
)}
|
|
1004
|
+
</div>
|
|
1005
|
+
);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
function TabButton({
|
|
1009
|
+
active,
|
|
1010
|
+
onClick,
|
|
1011
|
+
icon,
|
|
1012
|
+
label,
|
|
1013
|
+
badge,
|
|
1014
|
+
}: {
|
|
1015
|
+
active: boolean;
|
|
1016
|
+
onClick: () => void;
|
|
1017
|
+
icon: React.ReactNode;
|
|
1018
|
+
label: string;
|
|
1019
|
+
badge?: number;
|
|
1020
|
+
}) {
|
|
1021
|
+
return (
|
|
1022
|
+
<button
|
|
1023
|
+
onClick={onClick}
|
|
1024
|
+
className="flex-1 py-2 px-3 font-mono text-[9px] tracking-widest transition-all flex items-center justify-center gap-1.5"
|
|
1025
|
+
style={{
|
|
1026
|
+
background: active ? 'var(--color-surface, #fff)' : 'transparent',
|
|
1027
|
+
color: active ? 'var(--color-text, #0a0a0a)' : 'var(--color-text-muted, #888)',
|
|
1028
|
+
boxShadow: active ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
|
|
1029
|
+
}}
|
|
1030
|
+
>
|
|
1031
|
+
{icon}
|
|
1032
|
+
{label}
|
|
1033
|
+
{badge !== undefined && (
|
|
1034
|
+
<span
|
|
1035
|
+
className="ml-1 px-1.5 py-0.5 rounded-sm text-[8px] font-bold"
|
|
1036
|
+
style={{
|
|
1037
|
+
background: 'var(--color-info, #0047ff)',
|
|
1038
|
+
color: 'var(--color-surface, #fff)',
|
|
1039
|
+
}}
|
|
1040
|
+
>
|
|
1041
|
+
{badge}
|
|
1042
|
+
</span>
|
|
1043
|
+
)}
|
|
1044
|
+
</button>
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function AssetsTab({
|
|
1049
|
+
assets,
|
|
1050
|
+
loading,
|
|
1051
|
+
search,
|
|
1052
|
+
onSearchChange,
|
|
1053
|
+
tokenDataMap,
|
|
1054
|
+
ethPrice,
|
|
1055
|
+
showAddAsset,
|
|
1056
|
+
addAssetAnchor,
|
|
1057
|
+
onShowAddAsset,
|
|
1058
|
+
addAssetForm,
|
|
1059
|
+
onAddAssetFormChange,
|
|
1060
|
+
onAddAsset,
|
|
1061
|
+
addingAsset,
|
|
1062
|
+
chainOptions,
|
|
1063
|
+
selectedChain,
|
|
1064
|
+
onChainChange,
|
|
1065
|
+
onRemoveAsset,
|
|
1066
|
+
nativeCurrency,
|
|
1067
|
+
}: {
|
|
1068
|
+
assets: TrackedAsset[];
|
|
1069
|
+
loading: boolean;
|
|
1070
|
+
search: string;
|
|
1071
|
+
onSearchChange: (value: string) => void;
|
|
1072
|
+
tokenDataMap: Map<string, TokenData>;
|
|
1073
|
+
ethPrice: number | null;
|
|
1074
|
+
showAddAsset: boolean;
|
|
1075
|
+
addAssetAnchor: HTMLElement | null;
|
|
1076
|
+
onShowAddAsset: (show: boolean, anchor?: HTMLElement) => void;
|
|
1077
|
+
addAssetForm: { tokenAddress: string; symbol: string; name: string; chain: string };
|
|
1078
|
+
onAddAssetFormChange: (form: { tokenAddress: string; symbol: string; name: string; chain: string }) => void;
|
|
1079
|
+
onAddAsset: () => void;
|
|
1080
|
+
addingAsset: boolean;
|
|
1081
|
+
chainOptions: { value: string; label: string }[];
|
|
1082
|
+
selectedChain: string;
|
|
1083
|
+
onChainChange: (chain: string) => void;
|
|
1084
|
+
onRemoveAsset: (assetId: string) => void;
|
|
1085
|
+
nativeCurrency: string;
|
|
1086
|
+
}) {
|
|
1087
|
+
return (
|
|
1088
|
+
<div className="space-y-2">
|
|
1089
|
+
{/* Chain selector, Search, and Add - all on same line */}
|
|
1090
|
+
<div className="flex items-center gap-2">
|
|
1091
|
+
<div className="w-28">
|
|
1092
|
+
<ChainSelector
|
|
1093
|
+
value={selectedChain}
|
|
1094
|
+
onChange={onChainChange}
|
|
1095
|
+
chains={chainOptions.map(o => o.value)}
|
|
1096
|
+
size="sm"
|
|
1097
|
+
/>
|
|
1098
|
+
</div>
|
|
1099
|
+
<div className="flex-1">
|
|
1100
|
+
<TextInput
|
|
1101
|
+
value={search}
|
|
1102
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
1103
|
+
placeholder="Search tokens..."
|
|
1104
|
+
leftElement={<Search size={10} />}
|
|
1105
|
+
compact
|
|
1106
|
+
/>
|
|
1107
|
+
</div>
|
|
1108
|
+
<Button
|
|
1109
|
+
variant="primary"
|
|
1110
|
+
size="sm"
|
|
1111
|
+
onClick={(e) => onShowAddAsset(true, e.currentTarget as HTMLElement)}
|
|
1112
|
+
icon={<Plus size={10} />}
|
|
1113
|
+
/>
|
|
1114
|
+
</div>
|
|
1115
|
+
|
|
1116
|
+
{/* Add Asset Popover */}
|
|
1117
|
+
<Popover
|
|
1118
|
+
isOpen={showAddAsset}
|
|
1119
|
+
onClose={() => onShowAddAsset(false)}
|
|
1120
|
+
anchorEl={addAssetAnchor}
|
|
1121
|
+
title="ADD ASSET"
|
|
1122
|
+
anchor="right"
|
|
1123
|
+
>
|
|
1124
|
+
<div className="space-y-2 w-56">
|
|
1125
|
+
<div>
|
|
1126
|
+
<label className="font-mono text-[8px] tracking-widest block mb-1" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1127
|
+
TOKEN ADDRESS *
|
|
1128
|
+
</label>
|
|
1129
|
+
<input
|
|
1130
|
+
type="text"
|
|
1131
|
+
value={addAssetForm.tokenAddress}
|
|
1132
|
+
onChange={(e) => onAddAssetFormChange({ ...addAssetForm, tokenAddress: e.target.value })}
|
|
1133
|
+
placeholder="0x..."
|
|
1134
|
+
className="w-full px-2 py-1.5 font-mono text-[9px] focus:outline-none"
|
|
1135
|
+
style={{
|
|
1136
|
+
background: 'var(--color-surface, #fff)',
|
|
1137
|
+
border: '1px solid var(--color-border, #e5e5e5)',
|
|
1138
|
+
color: 'var(--color-text, #0a0a0a)',
|
|
1139
|
+
}}
|
|
1140
|
+
/>
|
|
1141
|
+
</div>
|
|
1142
|
+
<div className="flex gap-2">
|
|
1143
|
+
<div className="flex-1">
|
|
1144
|
+
<label className="font-mono text-[8px] tracking-widest block mb-1" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1145
|
+
SYMBOL
|
|
1146
|
+
</label>
|
|
1147
|
+
<input
|
|
1148
|
+
type="text"
|
|
1149
|
+
value={addAssetForm.symbol}
|
|
1150
|
+
onChange={(e) => onAddAssetFormChange({ ...addAssetForm, symbol: e.target.value })}
|
|
1151
|
+
placeholder="TKN"
|
|
1152
|
+
className="w-full px-2 py-1.5 font-mono text-[9px] focus:outline-none"
|
|
1153
|
+
style={{
|
|
1154
|
+
background: 'var(--color-surface, #fff)',
|
|
1155
|
+
border: '1px solid var(--color-border, #e5e5e5)',
|
|
1156
|
+
color: 'var(--color-text, #0a0a0a)',
|
|
1157
|
+
}}
|
|
1158
|
+
/>
|
|
1159
|
+
</div>
|
|
1160
|
+
<div className="w-28">
|
|
1161
|
+
<ChainSelector
|
|
1162
|
+
label="CHAIN"
|
|
1163
|
+
value={addAssetForm.chain}
|
|
1164
|
+
onChange={(chain) => onAddAssetFormChange({ ...addAssetForm, chain })}
|
|
1165
|
+
chains={chainOptions.map(o => o.value)}
|
|
1166
|
+
size="sm"
|
|
1167
|
+
/>
|
|
1168
|
+
</div>
|
|
1169
|
+
</div>
|
|
1170
|
+
<div>
|
|
1171
|
+
<label className="font-mono text-[8px] tracking-widest block mb-1" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1172
|
+
NAME
|
|
1173
|
+
</label>
|
|
1174
|
+
<input
|
|
1175
|
+
type="text"
|
|
1176
|
+
value={addAssetForm.name}
|
|
1177
|
+
onChange={(e) => onAddAssetFormChange({ ...addAssetForm, name: e.target.value })}
|
|
1178
|
+
placeholder="Token Name"
|
|
1179
|
+
className="w-full px-2 py-1.5 font-mono text-[9px] focus:outline-none"
|
|
1180
|
+
style={{
|
|
1181
|
+
background: 'var(--color-surface, #fff)',
|
|
1182
|
+
border: '1px solid var(--color-border, #e5e5e5)',
|
|
1183
|
+
color: 'var(--color-text, #0a0a0a)',
|
|
1184
|
+
}}
|
|
1185
|
+
/>
|
|
1186
|
+
</div>
|
|
1187
|
+
<Button
|
|
1188
|
+
onClick={onAddAsset}
|
|
1189
|
+
disabled={!addAssetForm.tokenAddress || addingAsset}
|
|
1190
|
+
loading={addingAsset}
|
|
1191
|
+
className="w-full"
|
|
1192
|
+
size="sm"
|
|
1193
|
+
>
|
|
1194
|
+
{addingAsset ? 'ADDING...' : 'ADD ASSET'}
|
|
1195
|
+
</Button>
|
|
1196
|
+
</div>
|
|
1197
|
+
</Popover>
|
|
1198
|
+
|
|
1199
|
+
{/* Assets List */}
|
|
1200
|
+
{loading && assets.length === 0 ? (
|
|
1201
|
+
<div className="py-6 text-center">
|
|
1202
|
+
<Loader2
|
|
1203
|
+
size={20}
|
|
1204
|
+
className="mx-auto mb-2 animate-spin"
|
|
1205
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
1206
|
+
/>
|
|
1207
|
+
<div className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1208
|
+
LOADING ASSETS...
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
) : assets.length === 0 ? (
|
|
1212
|
+
<div className="py-6 text-center">
|
|
1213
|
+
<Coins size={24} className="mx-auto mb-2" style={{ color: 'var(--color-text-muted, #888)' }} />
|
|
1214
|
+
<div className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1215
|
+
NO TRACKED ASSETS
|
|
1216
|
+
</div>
|
|
1217
|
+
<div className="font-mono text-[8px] mt-1" style={{ color: 'var(--color-text-faint, #aaa)' }}>
|
|
1218
|
+
Swap tokens to auto-track them
|
|
1219
|
+
</div>
|
|
1220
|
+
</div>
|
|
1221
|
+
) : (
|
|
1222
|
+
<div className="space-y-1 max-h-48 overflow-y-auto">
|
|
1223
|
+
{assets.map((asset) => {
|
|
1224
|
+
const tokenKey = isSolanaChain(selectedChain) ? asset.tokenAddress : asset.tokenAddress.toLowerCase();
|
|
1225
|
+
const tokenData = tokenDataMap.get(tokenKey);
|
|
1226
|
+
const balance = tokenData?.balance ?? asset.lastBalance ?? '0';
|
|
1227
|
+
const priceInEth = tokenData?.priceInEth ?? null;
|
|
1228
|
+
const usdValue = calculateUsdValue(balance, priceInEth, ethPrice);
|
|
1229
|
+
// Staleness indicator
|
|
1230
|
+
const balanceAge = asset.lastBalanceAt
|
|
1231
|
+
? Date.now() - new Date(asset.lastBalanceAt).getTime()
|
|
1232
|
+
: null;
|
|
1233
|
+
const stalenessColor = balanceAge === null ? 'var(--color-text-faint, #ccc)'
|
|
1234
|
+
: balanceAge < 30_000 ? 'var(--color-success, #00c853)'
|
|
1235
|
+
: balanceAge < 300_000 ? 'var(--color-info, #0047ff)'
|
|
1236
|
+
: 'var(--color-warning, #ff4d00)';
|
|
1237
|
+
|
|
1238
|
+
return (
|
|
1239
|
+
<div
|
|
1240
|
+
key={asset.id}
|
|
1241
|
+
className="p-2 rounded-sm group"
|
|
1242
|
+
style={{
|
|
1243
|
+
background: 'var(--color-surface, #fff)',
|
|
1244
|
+
border: '1px solid var(--color-border, #e5e5e5)',
|
|
1245
|
+
}}
|
|
1246
|
+
>
|
|
1247
|
+
<div className="flex items-center justify-between gap-2">
|
|
1248
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
1249
|
+
{/* Token icon placeholder */}
|
|
1250
|
+
<div
|
|
1251
|
+
className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 text-[10px] font-bold"
|
|
1252
|
+
style={{
|
|
1253
|
+
background: 'var(--color-background-alt, #f5f5f5)',
|
|
1254
|
+
color: 'var(--color-text-muted, #888)',
|
|
1255
|
+
}}
|
|
1256
|
+
>
|
|
1257
|
+
{asset.symbol?.charAt(0) || '?'}
|
|
1258
|
+
</div>
|
|
1259
|
+
<div className="min-w-0">
|
|
1260
|
+
<div
|
|
1261
|
+
className="font-mono text-[10px] font-bold truncate"
|
|
1262
|
+
style={{ color: 'var(--color-text, #0a0a0a)' }}
|
|
1263
|
+
>
|
|
1264
|
+
{asset.symbol || shortenAddress(asset.tokenAddress, 4)}
|
|
1265
|
+
</div>
|
|
1266
|
+
{asset.name && (
|
|
1267
|
+
<div
|
|
1268
|
+
className="font-mono text-[8px] truncate"
|
|
1269
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
1270
|
+
>
|
|
1271
|
+
{asset.name}
|
|
1272
|
+
</div>
|
|
1273
|
+
)}
|
|
1274
|
+
</div>
|
|
1275
|
+
</div>
|
|
1276
|
+
<div className="flex items-center gap-2">
|
|
1277
|
+
<div className="text-right shrink-0">
|
|
1278
|
+
<div className="flex items-center gap-1 justify-end">
|
|
1279
|
+
<div
|
|
1280
|
+
className="w-1 h-1 rounded-full shrink-0"
|
|
1281
|
+
style={{ background: stalenessColor }}
|
|
1282
|
+
title={asset.lastBalanceAt ? `Updated ${formatTimeAgo(asset.lastBalanceAt)}` : 'No cached balance'}
|
|
1283
|
+
/>
|
|
1284
|
+
<span
|
|
1285
|
+
className="font-mono text-[10px] font-bold"
|
|
1286
|
+
style={{ color: 'var(--color-text, #0a0a0a)' }}
|
|
1287
|
+
>
|
|
1288
|
+
{parseFloat(balance).toFixed(4)}
|
|
1289
|
+
</span>
|
|
1290
|
+
</div>
|
|
1291
|
+
{usdValue !== null && (
|
|
1292
|
+
<div className="font-mono text-[8px]" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1293
|
+
{formatUsdValue(usdValue)}
|
|
1294
|
+
</div>
|
|
1295
|
+
)}
|
|
1296
|
+
</div>
|
|
1297
|
+
<button
|
|
1298
|
+
onClick={() => onRemoveAsset(asset.id)}
|
|
1299
|
+
className="p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
1300
|
+
style={{ color: 'var(--color-warning, #ff4d00)' }}
|
|
1301
|
+
title="Remove asset"
|
|
1302
|
+
>
|
|
1303
|
+
<Trash2 size={10} />
|
|
1304
|
+
</button>
|
|
1305
|
+
</div>
|
|
1306
|
+
</div>
|
|
1307
|
+
{/* Pool info badge */}
|
|
1308
|
+
{asset.poolVersion && (
|
|
1309
|
+
<div className="mt-1.5 flex items-center gap-1">
|
|
1310
|
+
<span
|
|
1311
|
+
className="font-mono text-[7px] px-1 py-0.5 rounded-sm uppercase"
|
|
1312
|
+
style={{
|
|
1313
|
+
background: 'var(--color-background-alt, #f5f5f5)',
|
|
1314
|
+
color: 'var(--color-text-muted, #888)',
|
|
1315
|
+
}}
|
|
1316
|
+
>
|
|
1317
|
+
{asset.poolVersion}
|
|
1318
|
+
</span>
|
|
1319
|
+
{priceInEth !== null && (
|
|
1320
|
+
<span className="font-mono text-[7px]" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1321
|
+
{priceInEth.toFixed(8)} {nativeCurrency}
|
|
1322
|
+
</span>
|
|
1323
|
+
)}
|
|
1324
|
+
</div>
|
|
1325
|
+
)}
|
|
1326
|
+
</div>
|
|
1327
|
+
);
|
|
1328
|
+
})}
|
|
1329
|
+
</div>
|
|
1330
|
+
)}
|
|
1331
|
+
</div>
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function TransactionsTab({
|
|
1336
|
+
transactions,
|
|
1337
|
+
loading,
|
|
1338
|
+
search,
|
|
1339
|
+
onSearchChange,
|
|
1340
|
+
typeFilter,
|
|
1341
|
+
onTypeFilterChange,
|
|
1342
|
+
hasMore,
|
|
1343
|
+
onLoadMore,
|
|
1344
|
+
nativeCurrency,
|
|
1345
|
+
explorerUrl,
|
|
1346
|
+
}: {
|
|
1347
|
+
transactions: Transaction[];
|
|
1348
|
+
loading: boolean;
|
|
1349
|
+
search: string;
|
|
1350
|
+
onSearchChange: (value: string) => void;
|
|
1351
|
+
typeFilter: string;
|
|
1352
|
+
onTypeFilterChange: (value: string) => void;
|
|
1353
|
+
hasMore: boolean;
|
|
1354
|
+
onLoadMore: () => void;
|
|
1355
|
+
nativeCurrency: string;
|
|
1356
|
+
explorerUrl: string;
|
|
1357
|
+
}) {
|
|
1358
|
+
// Client-side search filter
|
|
1359
|
+
const filteredTx = search
|
|
1360
|
+
? transactions.filter(tx =>
|
|
1361
|
+
(tx.description?.toLowerCase().includes(search.toLowerCase())) ||
|
|
1362
|
+
(tx.txHash?.toLowerCase().includes(search.toLowerCase()))
|
|
1363
|
+
)
|
|
1364
|
+
: transactions;
|
|
1365
|
+
|
|
1366
|
+
return (
|
|
1367
|
+
<div className="space-y-2">
|
|
1368
|
+
{/* Type Filter and Search */}
|
|
1369
|
+
<div className="flex gap-2">
|
|
1370
|
+
<div className="w-24">
|
|
1371
|
+
<FilterDropdown
|
|
1372
|
+
options={TX_TYPE_OPTIONS}
|
|
1373
|
+
value={typeFilter}
|
|
1374
|
+
onChange={onTypeFilterChange}
|
|
1375
|
+
compact
|
|
1376
|
+
/>
|
|
1377
|
+
</div>
|
|
1378
|
+
<div className="flex-1">
|
|
1379
|
+
<TextInput
|
|
1380
|
+
value={search}
|
|
1381
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
1382
|
+
placeholder="Search tx..."
|
|
1383
|
+
leftElement={<Search size={10} />}
|
|
1384
|
+
compact
|
|
1385
|
+
/>
|
|
1386
|
+
</div>
|
|
1387
|
+
</div>
|
|
1388
|
+
|
|
1389
|
+
{/* Transactions List */}
|
|
1390
|
+
{loading && transactions.length === 0 ? (
|
|
1391
|
+
<div className="py-6 text-center">
|
|
1392
|
+
<Loader2
|
|
1393
|
+
size={20}
|
|
1394
|
+
className="mx-auto mb-2 animate-spin"
|
|
1395
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
1396
|
+
/>
|
|
1397
|
+
<div className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1398
|
+
LOADING TRANSACTIONS...
|
|
1399
|
+
</div>
|
|
1400
|
+
</div>
|
|
1401
|
+
) : filteredTx.length === 0 ? (
|
|
1402
|
+
<div className="py-6 text-center">
|
|
1403
|
+
<ArrowUpDown size={24} className="mx-auto mb-2" style={{ color: 'var(--color-text-muted, #888)' }} />
|
|
1404
|
+
<div className="font-mono text-[9px]" style={{ color: 'var(--color-text-muted, #888)' }}>
|
|
1405
|
+
NO TRANSACTIONS
|
|
1406
|
+
</div>
|
|
1407
|
+
<div className="font-mono text-[8px] mt-1" style={{ color: 'var(--color-text-faint, #aaa)' }}>
|
|
1408
|
+
{search ? 'Try a different search' : 'Transactions will appear here'}
|
|
1409
|
+
</div>
|
|
1410
|
+
</div>
|
|
1411
|
+
) : (
|
|
1412
|
+
<div className="space-y-1 max-h-48 overflow-y-auto">
|
|
1413
|
+
{filteredTx.map((tx) => {
|
|
1414
|
+
const typeColor = TX_TYPE_COLORS[tx.type] || 'var(--color-text-muted, #888)';
|
|
1415
|
+
|
|
1416
|
+
return (
|
|
1417
|
+
<div
|
|
1418
|
+
key={tx.id}
|
|
1419
|
+
className="p-2 rounded-sm"
|
|
1420
|
+
style={{
|
|
1421
|
+
background: 'var(--color-surface, #fff)',
|
|
1422
|
+
border: '1px solid var(--color-border, #e5e5e5)',
|
|
1423
|
+
}}
|
|
1424
|
+
>
|
|
1425
|
+
<div className="flex items-start justify-between gap-2">
|
|
1426
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
1427
|
+
<div
|
|
1428
|
+
className="w-1.5 h-1.5 rounded-full shrink-0"
|
|
1429
|
+
style={{ background: typeColor }}
|
|
1430
|
+
/>
|
|
1431
|
+
<div className="min-w-0">
|
|
1432
|
+
<div className="flex items-center gap-1.5">
|
|
1433
|
+
<span
|
|
1434
|
+
className="font-mono text-[8px] font-bold uppercase"
|
|
1435
|
+
style={{ color: typeColor }}
|
|
1436
|
+
>
|
|
1437
|
+
{tx.type}
|
|
1438
|
+
</span>
|
|
1439
|
+
{tx.amount && (
|
|
1440
|
+
<span
|
|
1441
|
+
className="font-mono text-[9px] font-bold"
|
|
1442
|
+
style={{ color: 'var(--color-text, #0a0a0a)' }}
|
|
1443
|
+
>
|
|
1444
|
+
{parseFloat(tx.amount).toFixed(4)} {nativeCurrency}
|
|
1445
|
+
</span>
|
|
1446
|
+
)}
|
|
1447
|
+
{tx.tokenAmount && (
|
|
1448
|
+
<span
|
|
1449
|
+
className="font-mono text-[9px] font-bold"
|
|
1450
|
+
style={{ color: 'var(--color-text, #0a0a0a)' }}
|
|
1451
|
+
>
|
|
1452
|
+
{parseFloat(tx.tokenAmount).toFixed(4)}
|
|
1453
|
+
</span>
|
|
1454
|
+
)}
|
|
1455
|
+
</div>
|
|
1456
|
+
{tx.description && (
|
|
1457
|
+
<div
|
|
1458
|
+
className="font-mono text-[8px] truncate max-w-[180px]"
|
|
1459
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
1460
|
+
>
|
|
1461
|
+
{tx.description}
|
|
1462
|
+
</div>
|
|
1463
|
+
)}
|
|
1464
|
+
</div>
|
|
1465
|
+
</div>
|
|
1466
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
1467
|
+
<span className="font-mono text-[7px]" style={{ color: 'var(--color-text-faint, #aaa)' }}>
|
|
1468
|
+
{formatTimeAgo(tx.createdAt)}
|
|
1469
|
+
</span>
|
|
1470
|
+
{tx.txHash && (
|
|
1471
|
+
<a
|
|
1472
|
+
href={`${explorerUrl}/tx/${tx.txHash}`}
|
|
1473
|
+
target="_blank"
|
|
1474
|
+
rel="noopener noreferrer"
|
|
1475
|
+
className="p-0.5 transition-colors"
|
|
1476
|
+
style={{ color: 'var(--color-text-muted, #888)' }}
|
|
1477
|
+
>
|
|
1478
|
+
<ExternalLink size={9} />
|
|
1479
|
+
</a>
|
|
1480
|
+
)}
|
|
1481
|
+
</div>
|
|
1482
|
+
</div>
|
|
1483
|
+
</div>
|
|
1484
|
+
);
|
|
1485
|
+
})}
|
|
1486
|
+
{hasMore && !search && (
|
|
1487
|
+
<button
|
|
1488
|
+
onClick={onLoadMore}
|
|
1489
|
+
disabled={loading}
|
|
1490
|
+
className="w-full py-2 font-mono text-[8px] transition-colors"
|
|
1491
|
+
style={{
|
|
1492
|
+
color: 'var(--color-text-muted, #888)',
|
|
1493
|
+
background: 'var(--color-background-alt, #f5f5f5)',
|
|
1494
|
+
}}
|
|
1495
|
+
>
|
|
1496
|
+
{loading ? 'LOADING...' : 'LOAD MORE'}
|
|
1497
|
+
</button>
|
|
1498
|
+
)}
|
|
1499
|
+
</div>
|
|
1500
|
+
)}
|
|
1501
|
+
</div>
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
export default WalletDetailApp;
|