@swarmclawai/swarmclaw 1.2.6 → 1.2.9
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/README.md +54 -23
- package/next.config.ts +1 -0
- package/package.json +4 -3
- package/scripts/easy-setup.mjs +1 -1
- package/scripts/postinstall.mjs +1 -1
- package/skills/swarmclaw.md +115 -0
- package/skills/tools/browser.md +131 -0
- package/skills/tools/execute.md +98 -0
- package/skills/tools/files.md +98 -0
- package/skills/tools/memory.md +104 -0
- package/skills/tools/platform.md +144 -0
- package/skills/tools/skills.md +83 -0
- package/src/app/agents/[id]/page.tsx +1 -18
- package/src/app/api/agents/thread-route.test.ts +0 -1
- package/src/app/api/approvals/route.test.ts +6 -22
- package/src/app/api/chats/[id]/messages/route.ts +23 -19
- package/src/app/api/chats/messages-route.test.ts +105 -51
- package/src/app/api/connectors/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
- package/src/app/api/openclaw/deploy/route.ts +2 -0
- package/src/app/api/portability/export/route.ts +8 -0
- package/src/app/api/portability/import/route.test.ts +80 -0
- package/src/app/api/portability/import/route.ts +28 -0
- package/src/app/api/settings/route.ts +0 -2
- package/src/app/api/setup/doctor/route.ts +4 -4
- package/src/app/api/wallets/[id]/route.ts +15 -157
- package/src/app/api/wallets/generate/route.ts +22 -0
- package/src/app/api/wallets/route.test.ts +147 -0
- package/src/app/api/wallets/route.ts +13 -95
- package/src/app/autonomy/page.tsx +2 -57
- package/src/app/protocols/page.tsx +2 -21
- package/src/app/settings/page.tsx +0 -9
- package/src/app/wallets/page.tsx +105 -5
- package/src/cli/index.js +21 -33
- package/src/cli/spec.js +19 -30
- package/src/components/agents/agent-chat-list.tsx +23 -1
- package/src/components/agents/agent-sheet.tsx +2 -40
- package/src/components/agents/inspector-panel.tsx +165 -131
- package/src/components/chat/chat-area.tsx +38 -9
- package/src/components/chat/chat-card.tsx +0 -31
- package/src/components/chat/message-bubble.tsx +1 -108
- package/src/components/chat/message-list.tsx +33 -19
- package/src/components/connectors/connector-sheet.tsx +25 -1
- package/src/components/gateways/gateway-sheet.tsx +5 -2
- package/src/components/layout/sidebar-rail.tsx +6 -10
- package/src/components/projects/project-detail.tsx +3 -35
- package/src/components/projects/tabs/overview-tab.tsx +3 -59
- package/src/components/projects/tabs/work-tab.tsx +7 -77
- package/src/components/protocols/structured-session-launcher.tsx +1 -22
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/tasks/task-card.tsx +4 -34
- package/src/components/tasks/task-sheet.tsx +6 -36
- package/src/components/wallets/wallet-list.tsx +150 -0
- package/src/lib/agent-execute-defaults.test.ts +24 -0
- package/src/lib/agent-execute-defaults.ts +62 -0
- package/src/lib/app/navigation.test.ts +0 -13
- package/src/lib/app/navigation.ts +2 -7
- package/src/lib/app/view-constants.ts +14 -19
- package/src/lib/chat/queued-message-queue.test.ts +134 -1
- package/src/lib/chat/queued-message-queue.ts +77 -2
- package/src/lib/server/agents/agent-service.ts +5 -0
- package/src/lib/server/agents/agent-thread-session.ts +0 -1
- package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
- package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
- package/src/lib/server/agents/delegation-jobs.ts +0 -25
- package/src/lib/server/agents/main-agent-loop.ts +1 -49
- package/src/lib/server/agents/subagent-runtime.ts +0 -1
- package/src/lib/server/approval-match.ts +0 -85
- package/src/lib/server/approvals.test.ts +6 -6
- package/src/lib/server/approvals.ts +0 -6
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
- package/src/lib/server/builtin-extensions.ts +1 -2
- package/src/lib/server/capability-router.test.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +15 -14
- package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -4
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +81 -64
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
- package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
- package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
- package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
- package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
- package/src/lib/server/chat-execution/message-classifier.ts +11 -16
- package/src/lib/server/chat-execution/prompt-builder.test.ts +27 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +14 -31
- package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
- package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
- package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
- package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +13 -126
- package/src/lib/server/chat-execution/stream-agent-chat.ts +46 -21
- package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
- package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
- package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
- package/src/lib/server/chats/chat-session-service.ts +3 -5
- package/src/lib/server/connectors/connector-inbound.ts +0 -1
- package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
- package/src/lib/server/connectors/connector-service.ts +39 -9
- package/src/lib/server/connectors/discord.ts +2 -2
- package/src/lib/server/connectors/matrix.ts +3 -2
- package/src/lib/server/connectors/signal.ts +5 -4
- package/src/lib/server/connectors/slack.ts +10 -9
- package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
- package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
- package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
- package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
- package/src/lib/server/connectors/swarmdock-tasks.ts +119 -0
- package/src/lib/server/connectors/swarmdock.ts +255 -0
- package/src/lib/server/connectors/teams.ts +3 -2
- package/src/lib/server/connectors/telegram.ts +4 -4
- package/src/lib/server/connectors/whatsapp.ts +2 -2
- package/src/lib/server/daemon/controller.ts +7 -0
- package/src/lib/server/execution-brief.test.ts +2 -25
- package/src/lib/server/execution-brief.ts +12 -35
- package/src/lib/server/execution-engine/task-attempt.ts +0 -1
- package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
- package/src/lib/server/messages/message-repository.test.ts +70 -0
- package/src/lib/server/messages/message-repository.ts +11 -6
- package/src/lib/server/openclaw/deploy.ts +32 -2
- package/src/lib/server/persistence/storage-context.ts +0 -5
- package/src/lib/server/plugins-advanced.test.ts +1 -2
- package/src/lib/server/portability/export.ts +109 -0
- package/src/lib/server/portability/import.ts +159 -0
- package/src/lib/server/protocols/protocol-normalization.ts +0 -4
- package/src/lib/server/protocols/protocol-queries.ts +0 -6
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
- package/src/lib/server/protocols/protocol-service.ts +0 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
- package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
- package/src/lib/server/protocols/protocol-swarm.ts +0 -2
- package/src/lib/server/protocols/protocol-types.ts +0 -2
- package/src/lib/server/provider-health.ts +1 -10
- package/src/lib/server/runtime/daemon-state/core.ts +0 -9
- package/src/lib/server/runtime/daemon-state.test.ts +0 -35
- package/src/lib/server/runtime/heartbeat-service.ts +3 -23
- package/src/lib/server/runtime/process-manager.ts +13 -9
- package/src/lib/server/runtime/queue/core.ts +11 -33
- package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
- package/src/lib/server/runtime/scheduler.ts +0 -13
- package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
- package/src/lib/server/runtime/session-run-manager/queries.ts +15 -1
- package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
- package/src/lib/server/runtime/session-run-manager.test.ts +58 -28
- package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
- package/src/lib/server/sandbox/session-runtime.ts +40 -28
- package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
- package/src/lib/server/session-tools/context.ts +1 -1
- package/src/lib/server/session-tools/credential-env.ts +109 -0
- package/src/lib/server/session-tools/crud.ts +3 -17
- package/src/lib/server/session-tools/delegate.ts +0 -4
- package/src/lib/server/session-tools/edit_file.ts +3 -2
- package/src/lib/server/session-tools/execute.test.ts +58 -0
- package/src/lib/server/session-tools/execute.ts +334 -0
- package/src/lib/server/session-tools/files-tool.ts +635 -0
- package/src/lib/server/session-tools/index.ts +14 -8
- package/src/lib/server/session-tools/memory-tool.ts +242 -0
- package/src/lib/server/session-tools/memory.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
- package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
- package/src/lib/server/session-tools/platform-tool.ts +617 -0
- package/src/lib/server/session-tools/session-info.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
- package/src/lib/server/session-tools/shell.ts +7 -122
- package/src/lib/server/session-tools/skills-tool.ts +396 -0
- package/src/lib/server/session-tools/team-context.ts +0 -3
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/storage-normalization.ts +10 -0
- package/src/lib/server/storage.ts +18 -45
- package/src/lib/server/tasks/task-checkout.ts +59 -0
- package/src/lib/server/tasks/task-lifecycle.ts +2 -0
- package/src/lib/server/tasks/task-route-service.ts +4 -26
- package/src/lib/server/tasks/task-service.ts +0 -7
- package/src/lib/server/tool-aliases.ts +2 -2
- package/src/lib/server/tool-capability-policy-advanced.test.ts +13 -6
- package/src/lib/server/tool-capability-policy.test.ts +2 -1
- package/src/lib/server/tool-capability-policy.ts +60 -35
- package/src/lib/server/tool-planning.ts +11 -12
- package/src/lib/server/universal-tool-access.ts +0 -1
- package/src/lib/server/wallets/wallet-crypto.ts +33 -0
- package/src/lib/server/wallets/wallet-repository.ts +24 -0
- package/src/lib/server/wallets/wallet-service.ts +119 -0
- package/src/lib/server/working-state/extraction.ts +8 -42
- package/src/lib/server/working-state/normalization.ts +10 -103
- package/src/lib/server/working-state/service.ts +12 -21
- package/src/lib/setup-defaults.ts +5 -0
- package/src/lib/strip-internal-metadata.test.ts +1 -1
- package/src/lib/strip-internal-metadata.ts +1 -1
- package/src/lib/tool-definitions.ts +1 -1
- package/src/lib/validation/schemas.test.ts +16 -0
- package/src/lib/validation/schemas.ts +49 -2
- package/src/stores/slices/data-slice.ts +5 -1
- package/src/stores/slices/ui-slice.ts +0 -4
- package/src/stores/use-chat-store.test.ts +231 -0
- package/src/stores/use-chat-store.ts +62 -13
- package/src/types/agent.ts +264 -0
- package/src/types/app-settings.ts +173 -0
- package/src/types/approval.ts +25 -0
- package/src/types/connector.ts +188 -0
- package/src/types/extension.ts +386 -0
- package/src/types/index.ts +16 -3555
- package/src/types/message.ts +56 -0
- package/src/types/misc.ts +737 -0
- package/src/types/protocol.ts +420 -0
- package/src/types/provider.ts +52 -0
- package/src/types/run.ts +180 -0
- package/src/types/schedule.ts +59 -0
- package/src/types/session.ts +215 -0
- package/src/types/skill.ts +157 -0
- package/src/types/swarmdock.ts +29 -0
- package/src/types/task.ts +144 -0
- package/src/types/working-state.ts +204 -0
- package/src/views/settings/section-heartbeat.tsx +2 -2
- package/src/views/settings/section-runtime-loop.tsx +0 -14
- package/src/app/api/canvas/[sessionId]/route.ts +0 -35
- package/src/app/api/missions/[id]/actions/route.ts +0 -31
- package/src/app/api/missions/[id]/events/route.ts +0 -14
- package/src/app/api/missions/[id]/route.ts +0 -10
- package/src/app/api/missions/route.test.ts +0 -244
- package/src/app/api/missions/route.ts +0 -57
- package/src/app/api/wallets/[id]/approve/route.ts +0 -79
- package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
- package/src/app/api/wallets/[id]/send/route.ts +0 -113
- package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
- package/src/app/missions/[id]/page.tsx +0 -3
- package/src/app/missions/page.tsx +0 -685
- package/src/components/canvas/canvas-panel.tsx +0 -267
- package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
- package/src/components/wallets/wallet-panel.tsx +0 -1010
- package/src/components/wallets/wallet-section.tsx +0 -260
- package/src/features/missions/queries.ts +0 -23
- package/src/lib/canvas-content.test.ts +0 -360
- package/src/lib/canvas-content.ts +0 -198
- package/src/lib/server/canvas-content.test.ts +0 -32
- package/src/lib/server/canvas-content.ts +0 -6
- package/src/lib/server/ethereum.ts +0 -591
- package/src/lib/server/evm-swap.ts +0 -476
- package/src/lib/server/missions/mission-intent.test.ts +0 -63
- package/src/lib/server/missions/mission-intent.ts +0 -569
- package/src/lib/server/missions/mission-repository.ts +0 -74
- package/src/lib/server/missions/mission-service/actions.ts +0 -6
- package/src/lib/server/missions/mission-service/bindings.ts +0 -9
- package/src/lib/server/missions/mission-service/context.ts +0 -4
- package/src/lib/server/missions/mission-service/core.ts +0 -2271
- package/src/lib/server/missions/mission-service/queries.ts +0 -12
- package/src/lib/server/missions/mission-service/recovery.ts +0 -5
- package/src/lib/server/missions/mission-service/ticks.ts +0 -9
- package/src/lib/server/missions/mission-service.test.ts +0 -888
- package/src/lib/server/missions/mission-service.ts +0 -6
- package/src/lib/server/session-tools/canvas.ts +0 -105
- package/src/lib/server/session-tools/sandbox.ts +0 -281
- package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
- package/src/lib/server/session-tools/wallet.ts +0 -1287
- package/src/lib/server/solana.ts +0 -327
- package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
- package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
- package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
- package/src/lib/server/wallet/wallet-service.test.ts +0 -81
- package/src/lib/server/wallet/wallet-service.ts +0 -225
- package/src/lib/wallet/wallet-transactions.test.ts +0 -75
- package/src/lib/wallet/wallet-transactions.ts +0 -43
- package/src/lib/wallet/wallet.test.ts +0 -333
- package/src/lib/wallet/wallet.ts +0 -183
- package/src/views/settings/section-wallets.tsx +0 -35
|
@@ -1,1010 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
|
4
|
-
import { api } from '@/lib/app/api-client'
|
|
5
|
-
import { copyTextToClipboard } from '@/lib/clipboard'
|
|
6
|
-
import { useAppStore } from '@/stores/use-app-store'
|
|
7
|
-
import { useNavigate } from '@/lib/app/navigation'
|
|
8
|
-
import { useWs } from '@/hooks/use-ws'
|
|
9
|
-
import { WalletApprovalDialog } from './wallet-approval-dialog'
|
|
10
|
-
import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
11
|
-
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
12
|
-
import type { AgentWallet, WalletTransaction, WalletBalanceSnapshot, WalletAssetBalance, WalletPortfolioSummary, Agent, WalletChain } from '@/types'
|
|
13
|
-
import {
|
|
14
|
-
SUPPORTED_WALLET_CHAINS,
|
|
15
|
-
formatWalletAmount,
|
|
16
|
-
getWalletAssetSymbol,
|
|
17
|
-
getWalletAtomicAmount,
|
|
18
|
-
getWalletBalanceAtomic,
|
|
19
|
-
getWalletChainMeta,
|
|
20
|
-
getWalletLimitAtomic,
|
|
21
|
-
parseDisplayAmountToAtomic,
|
|
22
|
-
} from '@/lib/wallet/wallet'
|
|
23
|
-
import { type WalletTransactionFilter, filterWalletTransactions, getWalletTransactionStatusGroup } from '@/lib/wallet/wallet-transactions'
|
|
24
|
-
import { toast } from 'sonner'
|
|
25
|
-
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
26
|
-
|
|
27
|
-
type SafeWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & {
|
|
28
|
-
balanceAtomic?: string
|
|
29
|
-
balanceLamports?: number
|
|
30
|
-
balanceFormatted?: string
|
|
31
|
-
balanceSymbol?: string
|
|
32
|
-
assets?: WalletAssetBalance[]
|
|
33
|
-
portfolioSummary?: WalletPortfolioSummary
|
|
34
|
-
isActive?: boolean
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getAgentWalletIds(agent: Agent | undefined | null): string[] {
|
|
38
|
-
const ids = Array.isArray(agent?.walletIds)
|
|
39
|
-
? agent.walletIds.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
40
|
-
: []
|
|
41
|
-
const legacy = typeof agent?.walletId === 'string' && agent.walletId.trim()
|
|
42
|
-
? [agent.walletId.trim()]
|
|
43
|
-
: []
|
|
44
|
-
return dedup([...ids, ...legacy])
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getAgentActiveWalletId(agent: Agent | undefined | null, fallbackWallets: SafeWallet[] = []): string | null {
|
|
48
|
-
const walletIds = getAgentWalletIds(agent)
|
|
49
|
-
if (typeof agent?.activeWalletId === 'string' && walletIds.includes(agent.activeWalletId)) return agent.activeWalletId
|
|
50
|
-
if (typeof agent?.walletId === 'string' && walletIds.includes(agent.walletId)) return agent.walletId
|
|
51
|
-
const activeWallet = fallbackWallets.find((wallet) => wallet.isActive)
|
|
52
|
-
return activeWallet?.id || fallbackWallets[0]?.id || walletIds[0] || null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function SolanaIcon({ size = 12, className = '', shimmer = false }: { size?: number; className?: string; shimmer?: boolean }) {
|
|
56
|
-
return (
|
|
57
|
-
<div className={`relative flex items-center justify-center ${className}`}>
|
|
58
|
-
<svg width={size} height={size} viewBox="0 0 128 128" className="relative z-10">
|
|
59
|
-
<defs>
|
|
60
|
-
<linearGradient id="sol-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
61
|
-
<stop offset="0%" stopColor="#00FFA3" />
|
|
62
|
-
<stop offset="100%" stopColor="#DC1FFF" />
|
|
63
|
-
</linearGradient>
|
|
64
|
-
</defs>
|
|
65
|
-
<path d="M25.5 100.5a4.3 4.3 0 0 1 3-1.3h93.2a2.2 2.2 0 0 1 1.5 3.7l-17.7 17.8a4.3 4.3 0 0 1-3 1.3H9.3a2.2 2.2 0 0 1-1.5-3.7l17.7-17.8z" fill="url(#sol-grad)" />
|
|
66
|
-
<path d="M25.5 7.3a4.4 4.4 0 0 1 3-1.3h93.2a2.2 2.2 0 0 1 1.5 3.7L105.5 27.5a4.3 4.3 0 0 1-3 1.3H9.3a2.2 2.2 0 0 1-1.5-3.7L25.5 7.3z" fill="url(#sol-grad)" />
|
|
67
|
-
<path d="M105.5 53.7a4.3 4.3 0 0 0-3-1.3H9.3a2.2 2.2 0 0 0-1.5 3.7l17.7 17.8a4.3 4.3 0 0 0 3 1.3h93.2a2.2 2.2 0 0 0 1.5-3.7L105.5 53.7z" fill="url(#sol-grad)" />
|
|
68
|
-
</svg>
|
|
69
|
-
{shimmer && (
|
|
70
|
-
<div className="absolute inset-0 bg-accent-bright/20 blur-md rounded-full animate-pulse" />
|
|
71
|
-
)}
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function EthereumIcon({ size = 12, className = '', shimmer = false }: { size?: number; className?: string; shimmer?: boolean }) {
|
|
77
|
-
return (
|
|
78
|
-
<div className={`relative flex items-center justify-center ${className}`}>
|
|
79
|
-
<svg width={size} height={size} viewBox="0 0 256 417" className="relative z-10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
80
|
-
<path d="M127.6 0L124.8 9.5V279.1L127.6 281.9L255.2 208.3L127.6 0Z" fill="#8A92B2" />
|
|
81
|
-
<path d="M127.6 0L0 208.3L127.6 281.9V151.1V0Z" fill="#62688F" />
|
|
82
|
-
<path d="M127.6 306.1L126 308V416.9L127.6 421.6L255.3 232.6L127.6 306.1Z" fill="#8A92B2" />
|
|
83
|
-
<path d="M127.6 421.6V306.1L0 232.6L127.6 421.6Z" fill="#62688F" />
|
|
84
|
-
<path d="M127.6 281.9L255.2 208.3L127.6 151.1V281.9Z" fill="#454A75" />
|
|
85
|
-
<path d="M0 208.3L127.6 281.9V151.1L0 208.3Z" fill="#8A92B2" />
|
|
86
|
-
</svg>
|
|
87
|
-
{shimmer && (
|
|
88
|
-
<div className="absolute inset-0 bg-sky-400/20 blur-md rounded-full animate-pulse" />
|
|
89
|
-
)}
|
|
90
|
-
</div>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function ChainIcon({ chain, size = 12, className = '', shimmer = false }: { chain: WalletChain; size?: number; className?: string; shimmer?: boolean }) {
|
|
95
|
-
if (chain === 'ethereum') return <EthereumIcon size={size} className={className} shimmer={shimmer} />
|
|
96
|
-
return <SolanaIcon size={size} className={className} shimmer={shimmer} />
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function walletBalanceLabel(wallet: SafeWallet): string {
|
|
100
|
-
return wallet.balanceFormatted || formatWalletAmount(wallet.chain, getWalletBalanceAtomic(wallet), { minFractionDigits: 3, maxFractionDigits: 6 })
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function walletAssetCountLabel(wallet: SafeWallet): string | null {
|
|
104
|
-
const count = wallet.portfolioSummary?.nonZeroAssets
|
|
105
|
-
if (!count) return null
|
|
106
|
-
return `${count} asset${count === 1 ? '' : 's'}`
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function suggestCreateChain(wallets: SafeWallet[], agentId?: string | null): WalletChain {
|
|
110
|
-
if (!agentId) return 'solana'
|
|
111
|
-
const connectedChains = new Set(wallets.filter((wallet) => wallet.agentId === agentId).map((wallet) => wallet.chain))
|
|
112
|
-
return SUPPORTED_WALLET_CHAINS.find((chain) => !connectedChains.has(chain)) || 'solana'
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function WalletPanel() {
|
|
116
|
-
const agents = useAppStore((s) => s.agents)
|
|
117
|
-
const appSettings = useAppStore((s) => s.appSettings)
|
|
118
|
-
const walletPanelAgentId = useAppStore((s) => s.walletPanelAgentId)
|
|
119
|
-
const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
|
|
120
|
-
const navigateTo = useNavigate()
|
|
121
|
-
const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
|
|
122
|
-
|
|
123
|
-
const [wallets, setWallets] = useState<Record<string, SafeWallet>>({})
|
|
124
|
-
const [selectedWalletId, setSelectedWalletId] = useState<string | null>(null)
|
|
125
|
-
const [transactions, setTransactions] = useState<WalletTransaction[]>([])
|
|
126
|
-
const [balanceHistory, setBalanceHistory] = useState<WalletBalanceSnapshot[]>([])
|
|
127
|
-
const [loading, setLoading] = useState(true)
|
|
128
|
-
const [transactionsLoading, setTransactionsLoading] = useState(false)
|
|
129
|
-
const [pendingApproval, setPendingApproval] = useState<WalletTransaction | null>(null)
|
|
130
|
-
const [transactionFilter, setTransactionFilter] = useState<WalletTransactionFilter>('all')
|
|
131
|
-
const [transactionQuery, setTransactionQuery] = useState('')
|
|
132
|
-
const detailRequestRef = useRef(0)
|
|
133
|
-
|
|
134
|
-
// Settings edit state
|
|
135
|
-
const [editingLimits, setEditingLimits] = useState(false)
|
|
136
|
-
const [perTxLimit, setPerTxLimit] = useState('')
|
|
137
|
-
const [dailyLimit, setDailyLimit] = useState('')
|
|
138
|
-
const [requireApproval, setRequireApproval] = useState(true)
|
|
139
|
-
const [saving, setSaving] = useState(false)
|
|
140
|
-
const [deleting, setDeleting] = useState(false)
|
|
141
|
-
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
142
|
-
const [settingDefault, setSettingDefault] = useState(false)
|
|
143
|
-
const [reassigning, setReassigning] = useState(false)
|
|
144
|
-
const [reassignSaving, setReassignSaving] = useState(false)
|
|
145
|
-
const [reassignError, setReassignError] = useState('')
|
|
146
|
-
|
|
147
|
-
// Create wallet state
|
|
148
|
-
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
149
|
-
const [createAgentId, setCreateAgentId] = useState('')
|
|
150
|
-
const [createChain, setCreateChain] = useState<WalletChain>('solana')
|
|
151
|
-
const [creating, setCreating] = useState(false)
|
|
152
|
-
const [createError, setCreateError] = useState('')
|
|
153
|
-
|
|
154
|
-
const loadWallets = useCallback(async () => {
|
|
155
|
-
try {
|
|
156
|
-
const data = await api<Record<string, SafeWallet>>('GET', '/wallets')
|
|
157
|
-
setWallets(data)
|
|
158
|
-
|
|
159
|
-
if (!walletPanelAgentId && !selectedWalletId && Object.keys(data).length > 0) {
|
|
160
|
-
const defaultWallet = Object.values(data).find((wallet) => wallet.isActive) || Object.values(data)[0]
|
|
161
|
-
if (defaultWallet) setSelectedWalletId(defaultWallet.id)
|
|
162
|
-
}
|
|
163
|
-
} catch { /* ignore */ }
|
|
164
|
-
setLoading(false)
|
|
165
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
166
|
-
}, [walletPanelAgentId])
|
|
167
|
-
|
|
168
|
-
useEffect(() => { loadWallets() }, [loadWallets])
|
|
169
|
-
|
|
170
|
-
// Sync wallet selection when agent panel changes
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
if (!walletPanelAgentId) return
|
|
173
|
-
const agentWallets = Object.values(wallets).filter((wallet) => wallet.agentId === walletPanelAgentId)
|
|
174
|
-
const selectedAgentWallet = selectedWalletId
|
|
175
|
-
? agentWallets.find((wallet) => wallet.id === selectedWalletId) || null
|
|
176
|
-
: null
|
|
177
|
-
if (selectedAgentWallet) {
|
|
178
|
-
setShowCreateForm(false)
|
|
179
|
-
setCreateError('')
|
|
180
|
-
setCreateAgentId(walletPanelAgentId)
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
const activeWalletId = getAgentActiveWalletId(agents[walletPanelAgentId] as Agent | undefined, agentWallets)
|
|
184
|
-
const match = agentWallets.find((wallet) => wallet.id === activeWalletId) || agentWallets[0]
|
|
185
|
-
if (match) {
|
|
186
|
-
setSelectedWalletId(match.id)
|
|
187
|
-
setShowCreateForm(false)
|
|
188
|
-
setCreateError('')
|
|
189
|
-
setCreateAgentId(walletPanelAgentId)
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
if (!agents[walletPanelAgentId]) return
|
|
193
|
-
setSelectedWalletId(null)
|
|
194
|
-
setShowCreateForm(true)
|
|
195
|
-
setCreateAgentId(walletPanelAgentId)
|
|
196
|
-
setCreateChain(suggestCreateChain(Object.values(wallets), walletPanelAgentId))
|
|
197
|
-
setCreateError('')
|
|
198
|
-
}, [agents, selectedWalletId, walletPanelAgentId, wallets])
|
|
199
|
-
|
|
200
|
-
// Load detail when wallet selected
|
|
201
|
-
const selectedWallet = selectedWalletId ? wallets[selectedWalletId] : null
|
|
202
|
-
|
|
203
|
-
const loadDetail = useCallback(async (walletId = selectedWalletId) => {
|
|
204
|
-
if (!walletId) return
|
|
205
|
-
const requestId = ++detailRequestRef.current
|
|
206
|
-
setTransactionsLoading(true)
|
|
207
|
-
const [detailResult, txResult, historyResult] = await Promise.allSettled([
|
|
208
|
-
api<SafeWallet>('GET', `/wallets/${walletId}`),
|
|
209
|
-
api<WalletTransaction[]>('GET', `/wallets/${walletId}/transactions`),
|
|
210
|
-
api<WalletBalanceSnapshot[]>('GET', `/wallets/${walletId}/balance-history`),
|
|
211
|
-
])
|
|
212
|
-
if (detailRequestRef.current !== requestId) return
|
|
213
|
-
|
|
214
|
-
if (detailResult.status === 'fulfilled') {
|
|
215
|
-
setWallets((prev) => ({ ...prev, [walletId]: detailResult.value }))
|
|
216
|
-
}
|
|
217
|
-
if (txResult.status === 'fulfilled') {
|
|
218
|
-
setTransactions(txResult.value)
|
|
219
|
-
const pending = txResult.value.find((tx) => tx.status === 'pending_approval')
|
|
220
|
-
setPendingApproval(pending || null)
|
|
221
|
-
}
|
|
222
|
-
if (historyResult.status === 'fulfilled') {
|
|
223
|
-
setBalanceHistory(historyResult.value)
|
|
224
|
-
}
|
|
225
|
-
setTransactionsLoading(false)
|
|
226
|
-
}, [selectedWalletId])
|
|
227
|
-
|
|
228
|
-
useEffect(() => { loadDetail() }, [loadDetail])
|
|
229
|
-
|
|
230
|
-
const refreshWalletData = useCallback(async () => {
|
|
231
|
-
await loadWallets()
|
|
232
|
-
if (selectedWalletId) {
|
|
233
|
-
await loadDetail(selectedWalletId)
|
|
234
|
-
}
|
|
235
|
-
}, [loadDetail, loadWallets, selectedWalletId])
|
|
236
|
-
|
|
237
|
-
useWs('wallets', refreshWalletData, 15000)
|
|
238
|
-
|
|
239
|
-
// Initialize limits when wallet selected
|
|
240
|
-
useEffect(() => {
|
|
241
|
-
if (selectedWallet) {
|
|
242
|
-
setPerTxLimit(formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'perTx'), { maxFractionDigits: 6 }))
|
|
243
|
-
setDailyLimit(formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'daily'), { maxFractionDigits: 6 }))
|
|
244
|
-
setRequireApproval(selectedWallet.requireApproval)
|
|
245
|
-
}
|
|
246
|
-
}, [selectedWallet])
|
|
247
|
-
|
|
248
|
-
const saveLimits = useCallback(async () => {
|
|
249
|
-
if (!selectedWalletId || !selectedWallet) return
|
|
250
|
-
setSaving(true)
|
|
251
|
-
try {
|
|
252
|
-
const spendingLimitAtomic = parseDisplayAmountToAtomic(perTxLimit || '0', getWalletChainMeta(selectedWallet.chain).decimals)
|
|
253
|
-
const dailyLimitAtomic = parseDisplayAmountToAtomic(dailyLimit || '0', getWalletChainMeta(selectedWallet.chain).decimals)
|
|
254
|
-
await api('PATCH', `/wallets/${selectedWalletId}`, {
|
|
255
|
-
spendingLimitAtomic,
|
|
256
|
-
dailyLimitAtomic,
|
|
257
|
-
requireApproval,
|
|
258
|
-
})
|
|
259
|
-
setEditingLimits(false)
|
|
260
|
-
loadDetail()
|
|
261
|
-
} catch (err: unknown) {
|
|
262
|
-
toast.error(errorMessage(err))
|
|
263
|
-
}
|
|
264
|
-
setSaving(false)
|
|
265
|
-
}, [selectedWalletId, selectedWallet, perTxLimit, dailyLimit, requireApproval, loadDetail])
|
|
266
|
-
|
|
267
|
-
const setDefaultWallet = useCallback(async () => {
|
|
268
|
-
if (!selectedWalletId) return
|
|
269
|
-
setSettingDefault(true)
|
|
270
|
-
try {
|
|
271
|
-
await api('PATCH', `/wallets/${selectedWalletId}`, { makeActive: true })
|
|
272
|
-
toast.success('Default wallet updated')
|
|
273
|
-
loadWallets()
|
|
274
|
-
} catch (err: unknown) {
|
|
275
|
-
toast.error(errorMessage(err))
|
|
276
|
-
}
|
|
277
|
-
setSettingDefault(false)
|
|
278
|
-
}, [selectedWalletId, loadWallets])
|
|
279
|
-
|
|
280
|
-
const handleDelete = useCallback(async () => {
|
|
281
|
-
if (!selectedWalletId) return
|
|
282
|
-
setDeleting(true)
|
|
283
|
-
try {
|
|
284
|
-
await api('DELETE', `/wallets/${selectedWalletId}`)
|
|
285
|
-
setSelectedWalletId(null)
|
|
286
|
-
setConfirmDelete(false)
|
|
287
|
-
loadWallets()
|
|
288
|
-
} catch { /* ignore */ }
|
|
289
|
-
setDeleting(false)
|
|
290
|
-
}, [selectedWalletId, loadWallets])
|
|
291
|
-
|
|
292
|
-
const [copied, setCopied] = useState(false)
|
|
293
|
-
const copyAddress = useCallback(async () => {
|
|
294
|
-
if (!selectedWallet) return
|
|
295
|
-
const copiedValue = await copyTextToClipboard(selectedWallet.publicKey)
|
|
296
|
-
if (!copiedValue) return
|
|
297
|
-
setCopied(true)
|
|
298
|
-
setTimeout(() => setCopied(false), 2000)
|
|
299
|
-
}, [selectedWallet])
|
|
300
|
-
|
|
301
|
-
const agentsMissingSelectedChain = useMemo(() => {
|
|
302
|
-
return Object.values(agents).filter((agent) => !Object.values(wallets).some((wallet) => wallet.agentId === agent.id && wallet.chain === createChain)) as Agent[]
|
|
303
|
-
}, [agents, createChain, wallets])
|
|
304
|
-
|
|
305
|
-
const canCreateMoreWallets = useMemo(() => {
|
|
306
|
-
return Object.values(agents).some((agent) =>
|
|
307
|
-
SUPPORTED_WALLET_CHAINS.some((chain) => !Object.values(wallets).some((wallet) => wallet.agentId === agent.id && wallet.chain === chain)),
|
|
308
|
-
)
|
|
309
|
-
}, [agents, wallets])
|
|
310
|
-
|
|
311
|
-
useEffect(() => {
|
|
312
|
-
if (!createAgentId) return
|
|
313
|
-
if (agentsMissingSelectedChain.some((agent) => agent.id === createAgentId)) return
|
|
314
|
-
setCreateAgentId('')
|
|
315
|
-
}, [agentsMissingSelectedChain, createAgentId])
|
|
316
|
-
|
|
317
|
-
const createWallet = useCallback(async () => {
|
|
318
|
-
if (!createAgentId) return
|
|
319
|
-
setCreating(true)
|
|
320
|
-
setCreateError('')
|
|
321
|
-
try {
|
|
322
|
-
await api('POST', '/wallets', { agentId: createAgentId, chain: createChain })
|
|
323
|
-
setShowCreateForm(false)
|
|
324
|
-
setCreateAgentId('')
|
|
325
|
-
setCreateChain('solana')
|
|
326
|
-
loadWallets()
|
|
327
|
-
} catch (err: unknown) {
|
|
328
|
-
setCreateError(errorMessage(err))
|
|
329
|
-
}
|
|
330
|
-
setCreating(false)
|
|
331
|
-
}, [createAgentId, createChain, loadWallets])
|
|
332
|
-
|
|
333
|
-
const filteredTransactions = useMemo(
|
|
334
|
-
() => filterWalletTransactions(transactions, { filter: transactionFilter, query: transactionQuery }),
|
|
335
|
-
[transactionFilter, transactionQuery, transactions],
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
if (loading) {
|
|
339
|
-
return (
|
|
340
|
-
<div className="flex-1 flex items-center justify-center">
|
|
341
|
-
<div className="animate-spin w-5 h-5 border-2 border-accent border-t-transparent rounded-full" />
|
|
342
|
-
</div>
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const walletList = Object.values(wallets).sort((a, b) => {
|
|
347
|
-
const aAgent = agents[a.agentId] as Agent | undefined
|
|
348
|
-
const bAgent = agents[b.agentId] as Agent | undefined
|
|
349
|
-
const aActive = a.isActive === true || getAgentActiveWalletId(aAgent, [a]) === a.id
|
|
350
|
-
const bActive = b.isActive === true || getAgentActiveWalletId(bAgent, [b]) === b.id
|
|
351
|
-
if (a.agentId === b.agentId && aActive !== bActive) return aActive ? -1 : 1
|
|
352
|
-
const agentCompare = (aAgent?.name || a.agentId).localeCompare(bAgent?.name || b.agentId)
|
|
353
|
-
if (agentCompare !== 0) return agentCompare
|
|
354
|
-
return a.chain.localeCompare(b.chain)
|
|
355
|
-
})
|
|
356
|
-
const selectedWalletMeta = selectedWallet ? getWalletChainMeta(selectedWallet.chain) : null
|
|
357
|
-
const selectedWalletSymbol = selectedWallet ? getWalletAssetSymbol(selectedWallet.chain) : null
|
|
358
|
-
const walletApprovalsEnabled = appSettings.walletApprovalsEnabled !== false
|
|
359
|
-
const selectedWalletBalance = selectedWallet ? walletBalanceLabel(selectedWallet) : null
|
|
360
|
-
const selectedWalletAssets = (selectedWallet?.assets || []).filter((asset) => BigInt(asset.balanceAtomic) > BigInt(0))
|
|
361
|
-
const selectedAgent = selectedWallet ? agents[selectedWallet.agentId] as Agent | undefined : undefined
|
|
362
|
-
const selectedAgentWallets = selectedWallet
|
|
363
|
-
? walletList.filter((wallet) => wallet.agentId === selectedWallet.agentId)
|
|
364
|
-
: []
|
|
365
|
-
const selectedAgentActiveWalletId = getAgentActiveWalletId(selectedAgent, selectedAgentWallets)
|
|
366
|
-
const reassignCandidates = selectedWallet
|
|
367
|
-
? (Object.values(agents).filter((agent) => (
|
|
368
|
-
agent.id !== selectedWallet.agentId
|
|
369
|
-
&& !walletList.some((wallet) => wallet.agentId === agent.id && wallet.chain === selectedWallet.chain)
|
|
370
|
-
)) as Agent[])
|
|
371
|
-
: []
|
|
372
|
-
|
|
373
|
-
if (walletList.length === 0) {
|
|
374
|
-
return (
|
|
375
|
-
<div className="flex-1 flex items-center justify-center">
|
|
376
|
-
<div className="text-center max-w-sm" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
|
|
377
|
-
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="mx-auto mb-4 text-text-3/30">
|
|
378
|
-
<rect x="2" y="6" width="20" height="14" rx="2" /><path d="M22 10H18a2 2 0 0 0 0 4h4" /><path d="M6 6V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2" />
|
|
379
|
-
</svg>
|
|
380
|
-
<h3 className="font-display text-[14px] font-600 text-text-2 mb-2">No wallets yet</h3>
|
|
381
|
-
{agentsMissingSelectedChain.length > 0 ? (
|
|
382
|
-
<div className="mt-4 space-y-3">
|
|
383
|
-
<AgentPickerList
|
|
384
|
-
agents={agentsMissingSelectedChain}
|
|
385
|
-
selected={createAgentId}
|
|
386
|
-
onSelect={(id) => setCreateAgentId(id === createAgentId ? '' : id)}
|
|
387
|
-
maxHeight={180}
|
|
388
|
-
/>
|
|
389
|
-
<select
|
|
390
|
-
value={createChain}
|
|
391
|
-
onChange={(e) => setCreateChain(e.target.value as WalletChain)}
|
|
392
|
-
className="w-full px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
|
|
393
|
-
style={{ fontFamily: 'inherit' }}
|
|
394
|
-
>
|
|
395
|
-
<option value="solana">Solana</option>
|
|
396
|
-
<option value="ethereum">Ethereum (EVM)</option>
|
|
397
|
-
</select>
|
|
398
|
-
<button
|
|
399
|
-
type="button"
|
|
400
|
-
onClick={createWallet}
|
|
401
|
-
disabled={!createAgentId || creating}
|
|
402
|
-
className="w-full px-3 py-2 rounded-[8px] bg-accent text-white text-[12px] font-600 hover:brightness-110 cursor-pointer disabled:opacity-50 transition-colors"
|
|
403
|
-
style={{ fontFamily: 'inherit' }}
|
|
404
|
-
>
|
|
405
|
-
{creating ? 'Creating...' : 'Create Wallet'}
|
|
406
|
-
</button>
|
|
407
|
-
{createError && <p className="text-[11px] text-red-400">{createError}</p>}
|
|
408
|
-
</div>
|
|
409
|
-
) : (
|
|
410
|
-
<p className="text-[12px] text-text-3/60">
|
|
411
|
-
Every agent already has a {getWalletChainMeta(createChain).label} wallet.
|
|
412
|
-
</p>
|
|
413
|
-
)}
|
|
414
|
-
</div>
|
|
415
|
-
</div>
|
|
416
|
-
)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return (
|
|
420
|
-
<div className="flex-1 flex h-full min-w-0">
|
|
421
|
-
{/* Sidebar — wallet list */}
|
|
422
|
-
<div className="w-[240px] shrink-0 border-r border-white/[0.06] flex flex-col">
|
|
423
|
-
<div className="flex items-center px-4 pt-4 pb-2 shrink-0">
|
|
424
|
-
<h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] flex-1">Wallets</h2>
|
|
425
|
-
<button
|
|
426
|
-
type="button"
|
|
427
|
-
onClick={() => {
|
|
428
|
-
setShowCreateForm(!showCreateForm)
|
|
429
|
-
setCreateAgentId(walletPanelAgentId || '')
|
|
430
|
-
setCreateChain(suggestCreateChain(walletList, walletPanelAgentId))
|
|
431
|
-
setCreateError('')
|
|
432
|
-
}}
|
|
433
|
-
disabled={!canCreateMoreWallets}
|
|
434
|
-
className="w-6 h-6 rounded-[6px] flex items-center justify-center text-text-3/50 hover:text-text-2 hover:bg-white/[0.06] transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-default"
|
|
435
|
-
title={canCreateMoreWallets ? 'Create wallet' : 'Every agent already has both wallet types'}
|
|
436
|
-
>
|
|
437
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
438
|
-
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
|
|
439
|
-
</svg>
|
|
440
|
-
</button>
|
|
441
|
-
</div>
|
|
442
|
-
<div className="flex-1 overflow-y-auto px-2 pb-4">
|
|
443
|
-
{showCreateForm && (
|
|
444
|
-
<div className="mx-1 mb-2 p-2.5 rounded-[8px] border border-accent/20 bg-accent-soft/10 space-y-2"
|
|
445
|
-
style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}>
|
|
446
|
-
<AgentPickerList
|
|
447
|
-
agents={agentsMissingSelectedChain}
|
|
448
|
-
selected={createAgentId}
|
|
449
|
-
onSelect={(id) => setCreateAgentId(id === createAgentId ? '' : id)}
|
|
450
|
-
maxHeight={160}
|
|
451
|
-
/>
|
|
452
|
-
<select
|
|
453
|
-
value={createChain}
|
|
454
|
-
onChange={(e) => setCreateChain(e.target.value as WalletChain)}
|
|
455
|
-
className="w-full px-2 py-1.5 rounded-[6px] border border-white/[0.08] bg-surface text-[10px] text-text-1 outline-none focus:border-accent/40"
|
|
456
|
-
style={{ fontFamily: 'inherit' }}
|
|
457
|
-
>
|
|
458
|
-
<option value="solana">Solana</option>
|
|
459
|
-
<option value="ethereum">Ethereum (EVM)</option>
|
|
460
|
-
</select>
|
|
461
|
-
<div className="flex gap-1.5">
|
|
462
|
-
<button
|
|
463
|
-
type="button"
|
|
464
|
-
onClick={createWallet}
|
|
465
|
-
disabled={!createAgentId || creating}
|
|
466
|
-
className="flex-1 px-2 py-1.5 rounded-[6px] bg-accent text-white text-[10px] font-600 hover:brightness-110 cursor-pointer disabled:opacity-50 transition-colors"
|
|
467
|
-
style={{ fontFamily: 'inherit' }}
|
|
468
|
-
>
|
|
469
|
-
{creating ? 'Creating...' : 'Create'}
|
|
470
|
-
</button>
|
|
471
|
-
<button
|
|
472
|
-
type="button"
|
|
473
|
-
onClick={() => { setShowCreateForm(false); setCreateError('') }}
|
|
474
|
-
className="px-2 py-1.5 rounded-[6px] border border-white/[0.08] text-text-3 text-[10px] hover:text-text-2 cursor-pointer transition-colors"
|
|
475
|
-
style={{ fontFamily: 'inherit' }}
|
|
476
|
-
>
|
|
477
|
-
Cancel
|
|
478
|
-
</button>
|
|
479
|
-
</div>
|
|
480
|
-
{createError && <p className="text-[10px] text-red-400">{createError}</p>}
|
|
481
|
-
</div>
|
|
482
|
-
)}
|
|
483
|
-
{walletList.map((w, idx) => {
|
|
484
|
-
const a = agents[w.agentId] as Agent | undefined
|
|
485
|
-
const isActive = w.isActive === true || getAgentActiveWalletId(a, walletList.filter((wallet) => wallet.agentId === w.agentId)) === w.id
|
|
486
|
-
return (
|
|
487
|
-
<button
|
|
488
|
-
key={w.id}
|
|
489
|
-
onClick={() => { setSelectedWalletId(w.id); setWalletPanelAgentId(w.agentId) }}
|
|
490
|
-
className={`w-full text-left px-3 py-2.5 rounded-[8px] mb-1 transition-all cursor-pointer flex items-center gap-2.5 hover:scale-[1.02] ${
|
|
491
|
-
selectedWalletId === w.id ? 'bg-accent-soft/30 text-text-1' : 'text-text-3 hover:bg-white/[0.04]'
|
|
492
|
-
}`}
|
|
493
|
-
style={{
|
|
494
|
-
animation: 'fade-up 0.4s var(--ease-spring) both',
|
|
495
|
-
animationDelay: `${idx * 0.03}s`
|
|
496
|
-
}}
|
|
497
|
-
>
|
|
498
|
-
<AgentAvatar seed={a?.avatarSeed || null} avatarUrl={a?.avatarUrl} name={a?.name || '?'} size={28} />
|
|
499
|
-
<div className="flex-1 min-w-0">
|
|
500
|
-
<div className="flex items-center gap-1.5 min-w-0">
|
|
501
|
-
<div className="text-[12px] font-600 truncate">{a?.name || w.agentId}</div>
|
|
502
|
-
{isActive && (
|
|
503
|
-
<span className="shrink-0 px-1 py-0.5 rounded-[999px] bg-accent-soft/40 text-accent-bright text-[8px] font-700 uppercase tracking-wide">
|
|
504
|
-
Default
|
|
505
|
-
</span>
|
|
506
|
-
)}
|
|
507
|
-
</div>
|
|
508
|
-
<div className="text-[10px] text-text-3/50 font-mono truncate mt-0.5 flex items-center gap-1">
|
|
509
|
-
<ChainIcon chain={w.chain} size={9} className="shrink-0 opacity-50" />
|
|
510
|
-
<span className="truncate">{w.publicKey.slice(0, 8)}...{w.publicKey.slice(-4)}</span>
|
|
511
|
-
<span className="text-text-3/40">{walletBalanceLabel(w)} {getWalletAssetSymbol(w.chain)}</span>
|
|
512
|
-
{walletAssetCountLabel(w) && <span className="text-text-3/35">{walletAssetCountLabel(w)}</span>}
|
|
513
|
-
</div>
|
|
514
|
-
</div>
|
|
515
|
-
</button>
|
|
516
|
-
)
|
|
517
|
-
})}
|
|
518
|
-
</div>
|
|
519
|
-
</div>
|
|
520
|
-
|
|
521
|
-
{/* Main detail area */}
|
|
522
|
-
{selectedWallet ? (
|
|
523
|
-
<div className="flex-1 overflow-y-auto p-6 space-y-6" key={selectedWallet.id}>
|
|
524
|
-
{/* Warning banner */}
|
|
525
|
-
<div className="flex items-start gap-3 p-3 rounded-[10px] bg-amber-500/10 border border-amber-500/20"
|
|
526
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
|
|
527
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-amber-400 shrink-0 mt-0.5">
|
|
528
|
-
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
529
|
-
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
|
|
530
|
-
</svg>
|
|
531
|
-
<p className="text-[11px] text-amber-300/80 leading-relaxed">
|
|
532
|
-
Agent Wallets is experimental. Crypto transactions are irreversible. Do not store more than you can afford to lose.
|
|
533
|
-
</p>
|
|
534
|
-
</div>
|
|
535
|
-
|
|
536
|
-
{selectedAgentWallets.length > 1 && (
|
|
537
|
-
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
538
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.03s both' }}>
|
|
539
|
-
<div className="flex items-center justify-between gap-3 mb-3">
|
|
540
|
-
<div>
|
|
541
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600">Combined Wallet Stats</div>
|
|
542
|
-
<p className="text-[12px] text-text-3/70 mt-1">
|
|
543
|
-
{selectedAgentWallets.length} wallets connected for this agent. Pick a wallet in the sidebar for chain-specific history and controls.
|
|
544
|
-
</p>
|
|
545
|
-
</div>
|
|
546
|
-
<div className="text-right">
|
|
547
|
-
<div className="text-[18px] font-600 text-text-1">{selectedAgentWallets.length}</div>
|
|
548
|
-
<div className="text-[10px] uppercase tracking-wide text-text-3/50">Wallets</div>
|
|
549
|
-
</div>
|
|
550
|
-
</div>
|
|
551
|
-
<div className="grid gap-2 md:grid-cols-2">
|
|
552
|
-
{selectedAgentWallets.map((wallet) => (
|
|
553
|
-
<div key={wallet.id} className="rounded-[10px] border border-white/[0.06] bg-black/10 px-3 py-2">
|
|
554
|
-
<div className="flex items-center gap-2">
|
|
555
|
-
<span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">
|
|
556
|
-
{getWalletChainMeta(wallet.chain).label}
|
|
557
|
-
</span>
|
|
558
|
-
{(wallet.id === selectedAgentActiveWalletId || wallet.isActive) && (
|
|
559
|
-
<span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/40 text-accent-bright text-[8px] font-700 uppercase tracking-wide">
|
|
560
|
-
Default
|
|
561
|
-
</span>
|
|
562
|
-
)}
|
|
563
|
-
</div>
|
|
564
|
-
<div className="mt-1 text-[14px] font-600 text-text-1">
|
|
565
|
-
{walletBalanceLabel(wallet)} {getWalletAssetSymbol(wallet.chain)}
|
|
566
|
-
</div>
|
|
567
|
-
{wallet.portfolioSummary?.nonZeroAssets ? (
|
|
568
|
-
<div className="mt-1 text-[10px] text-text-3/55">
|
|
569
|
-
{wallet.portfolioSummary.nonZeroAssets} detected asset{wallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'}
|
|
570
|
-
</div>
|
|
571
|
-
) : null}
|
|
572
|
-
<div className="mt-1 text-[10px] text-text-3/55 font-mono truncate">{wallet.publicKey}</div>
|
|
573
|
-
</div>
|
|
574
|
-
))}
|
|
575
|
-
</div>
|
|
576
|
-
</div>
|
|
577
|
-
)}
|
|
578
|
-
|
|
579
|
-
{/* Agent & Address */}
|
|
580
|
-
<div style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.05s both' }}>
|
|
581
|
-
<div className="flex items-center gap-2 mb-2">
|
|
582
|
-
{(() => {
|
|
583
|
-
const a = agents[selectedWallet.agentId] as Agent | undefined
|
|
584
|
-
return a ? (
|
|
585
|
-
<button
|
|
586
|
-
type="button"
|
|
587
|
-
onClick={() => { setEditingAgentId(a.id); navigateTo('agents') }}
|
|
588
|
-
className="flex items-center gap-2 bg-transparent border-none p-0 cursor-pointer group"
|
|
589
|
-
style={{ fontFamily: 'inherit' }}
|
|
590
|
-
title="Open agent settings"
|
|
591
|
-
>
|
|
592
|
-
<AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={24} />
|
|
593
|
-
<span className="text-[13px] font-600 text-text-2 group-hover:text-accent-bright transition-colors">{a.name}</span>
|
|
594
|
-
</button>
|
|
595
|
-
) : (
|
|
596
|
-
<span className="text-[13px] font-600 text-text-2">{selectedWallet.agentId}</span>
|
|
597
|
-
)
|
|
598
|
-
})()}
|
|
599
|
-
<span className="inline-flex items-center gap-1 text-[11px] text-text-3/40 uppercase tracking-wide font-600">
|
|
600
|
-
<ChainIcon chain={selectedWallet.chain} size={11} />
|
|
601
|
-
{selectedWallet.chain}
|
|
602
|
-
</span>
|
|
603
|
-
{(selectedWallet.id === selectedAgentActiveWalletId || selectedWallet.isActive) && (
|
|
604
|
-
<span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/40 text-accent-bright text-[9px] font-700 uppercase tracking-wide">
|
|
605
|
-
Default
|
|
606
|
-
</span>
|
|
607
|
-
)}
|
|
608
|
-
<button
|
|
609
|
-
type="button"
|
|
610
|
-
onClick={() => { setReassigning(!reassigning); setReassignError('') }}
|
|
611
|
-
className="text-[10px] text-text-3/40 hover:text-accent-bright transition-colors cursor-pointer bg-transparent border-none px-1.5 py-0.5 rounded-[5px] hover:bg-white/[0.04]"
|
|
612
|
-
style={{ fontFamily: 'inherit' }}
|
|
613
|
-
>
|
|
614
|
-
{reassigning ? 'Cancel' : 'Reassign'}
|
|
615
|
-
</button>
|
|
616
|
-
{selectedWallet.id !== selectedAgentActiveWalletId && !selectedWallet.isActive && (
|
|
617
|
-
<button
|
|
618
|
-
type="button"
|
|
619
|
-
onClick={setDefaultWallet}
|
|
620
|
-
disabled={settingDefault}
|
|
621
|
-
className="text-[10px] text-accent-bright hover:text-white transition-colors cursor-pointer bg-transparent border border-accent-bright/20 px-1.5 py-0.5 rounded-[5px] hover:bg-accent/20 disabled:opacity-50"
|
|
622
|
-
style={{ fontFamily: 'inherit' }}
|
|
623
|
-
>
|
|
624
|
-
{settingDefault ? 'Setting...' : 'Set Default'}
|
|
625
|
-
</button>
|
|
626
|
-
)}
|
|
627
|
-
</div>
|
|
628
|
-
{reassigning && (
|
|
629
|
-
<div className="mb-2 space-y-2" style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}>
|
|
630
|
-
<p className="text-[11px] text-text-3/60">Select a new agent to control this wallet:</p>
|
|
631
|
-
{reassignCandidates.length > 0 ? (
|
|
632
|
-
<AgentPickerList
|
|
633
|
-
agents={reassignCandidates}
|
|
634
|
-
selected=""
|
|
635
|
-
onSelect={async (agentId) => {
|
|
636
|
-
setReassignSaving(true)
|
|
637
|
-
setReassignError('')
|
|
638
|
-
try {
|
|
639
|
-
await api('PATCH', `/wallets/${selectedWallet.id}`, { agentId })
|
|
640
|
-
setReassigning(false)
|
|
641
|
-
loadWallets()
|
|
642
|
-
} catch (err: unknown) {
|
|
643
|
-
setReassignError(errorMessage(err) || 'Reassign failed')
|
|
644
|
-
}
|
|
645
|
-
setReassignSaving(false)
|
|
646
|
-
}}
|
|
647
|
-
maxHeight={160}
|
|
648
|
-
/>
|
|
649
|
-
) : (
|
|
650
|
-
<p className="text-[10px] text-text-3/50">
|
|
651
|
-
No other agents can take this {selectedWallet.chain} wallet right now.
|
|
652
|
-
</p>
|
|
653
|
-
)}
|
|
654
|
-
{reassignSaving && <p className="text-[10px] text-text-3/50">Reassigning...</p>}
|
|
655
|
-
{reassignError && <p className="text-[10px] text-red-400">{reassignError}</p>}
|
|
656
|
-
</div>
|
|
657
|
-
)}
|
|
658
|
-
<div className="flex items-center gap-2">
|
|
659
|
-
<code className="text-[13px] text-text-2 font-mono bg-black/20 px-3 py-2 rounded-[8px] flex-1 truncate">
|
|
660
|
-
{selectedWallet.publicKey}
|
|
661
|
-
</code>
|
|
662
|
-
<button
|
|
663
|
-
type="button"
|
|
664
|
-
onClick={copyAddress}
|
|
665
|
-
className="shrink-0 px-3 py-2 rounded-[8px] text-[11px] text-text-3 hover:text-text-2 border border-white/[0.08] bg-surface transition-colors cursor-pointer"
|
|
666
|
-
style={{ fontFamily: 'inherit' }}
|
|
667
|
-
>
|
|
668
|
-
{copied ? 'Copied!' : 'Copy'}
|
|
669
|
-
</button>
|
|
670
|
-
</div>
|
|
671
|
-
</div>
|
|
672
|
-
|
|
673
|
-
{/* Balance card */}
|
|
674
|
-
<div className="p-5 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
675
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.1s both' }}>
|
|
676
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-2">Balance</div>
|
|
677
|
-
<div className="flex items-baseline gap-3">
|
|
678
|
-
<div className="text-[28px] font-600 text-text-1 tracking-tight">
|
|
679
|
-
{selectedWalletBalance} <span className="text-[14px] text-text-3/60 font-mono">{selectedWalletSymbol}</span>
|
|
680
|
-
</div>
|
|
681
|
-
<ChainIcon chain={selectedWallet.chain} size={16} shimmer className="opacity-80" />
|
|
682
|
-
</div>
|
|
683
|
-
<div className="mt-2 text-[11px] text-text-3/60">
|
|
684
|
-
{selectedWallet.portfolioSummary?.nonZeroAssets
|
|
685
|
-
? `${selectedWallet.portfolioSummary.nonZeroAssets} funded asset${selectedWallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'} across ${Math.max(selectedWallet.portfolioSummary.networkCount, 1)} network${selectedWallet.portfolioSummary.networkCount === 1 ? '' : 's'}`
|
|
686
|
-
: 'No funded assets detected yet.'}
|
|
687
|
-
</div>
|
|
688
|
-
</div>
|
|
689
|
-
|
|
690
|
-
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
691
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.13s both' }}>
|
|
692
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Detected Assets</div>
|
|
693
|
-
{selectedWalletAssets.length === 0 ? (
|
|
694
|
-
<p className="text-[12px] text-text-3/55">No funded token or native balances detected yet.</p>
|
|
695
|
-
) : (
|
|
696
|
-
<div className="space-y-2">
|
|
697
|
-
{selectedWalletAssets.map((asset) => (
|
|
698
|
-
<div key={asset.id} className="flex items-center justify-between gap-3 rounded-[10px] border border-white/[0.06] bg-black/10 px-3 py-2">
|
|
699
|
-
<div className="min-w-0">
|
|
700
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
701
|
-
<span className="text-[12px] font-600 text-text-1 truncate">{asset.symbol}</span>
|
|
702
|
-
<span className="text-[10px] text-text-3/55 uppercase tracking-wide">{asset.networkLabel}</span>
|
|
703
|
-
{asset.isNative && (
|
|
704
|
-
<span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/30 text-accent-bright text-[8px] font-700 uppercase tracking-wide">
|
|
705
|
-
Gas
|
|
706
|
-
</span>
|
|
707
|
-
)}
|
|
708
|
-
</div>
|
|
709
|
-
<div className="text-[10px] text-text-3/55 truncate">{asset.name || asset.symbol}</div>
|
|
710
|
-
</div>
|
|
711
|
-
<div className="text-right shrink-0">
|
|
712
|
-
<div className="text-[12px] font-600 text-text-1">{asset.balanceDisplay || asset.balanceFormatted || asset.balanceAtomic}</div>
|
|
713
|
-
{asset.contractAddress && (
|
|
714
|
-
<div className="text-[10px] text-text-3/45 font-mono">{asset.contractAddress.slice(0, 6)}...{asset.contractAddress.slice(-4)}</div>
|
|
715
|
-
)}
|
|
716
|
-
{asset.tokenMint && (
|
|
717
|
-
<div className="text-[10px] text-text-3/45 font-mono">{asset.tokenMint.slice(0, 6)}...{asset.tokenMint.slice(-4)}</div>
|
|
718
|
-
)}
|
|
719
|
-
</div>
|
|
720
|
-
</div>
|
|
721
|
-
))}
|
|
722
|
-
</div>
|
|
723
|
-
)}
|
|
724
|
-
</div>
|
|
725
|
-
|
|
726
|
-
{/* Funding help */}
|
|
727
|
-
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
728
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.16s both' }}>
|
|
729
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-2">How to Fund This Wallet</div>
|
|
730
|
-
<div className="space-y-2 text-[12px] text-text-3/70 leading-relaxed">
|
|
731
|
-
{selectedWalletMeta?.fundingInstructions.map((line) => (
|
|
732
|
-
<p key={line}>{line}</p>
|
|
733
|
-
))}
|
|
734
|
-
<p className="text-text-3/50 text-[11px]">The private key is AES-256 encrypted in your local database (<code className="text-[10px] bg-black/20 px-1 py-0.5 rounded">data/swarmclaw.db</code>). It is never exposed via the API. To export it, query the <code className="text-[10px] bg-black/20 px-1 py-0.5 rounded">wallets</code> table directly and decrypt using your <code className="text-[10px] bg-black/20 px-1 py-0.5 rounded">CREDENTIAL_SECRET</code>.</p>
|
|
735
|
-
</div>
|
|
736
|
-
</div>
|
|
737
|
-
|
|
738
|
-
{/* Balance history chart (simple) */}
|
|
739
|
-
{balanceHistory.length > 1 && (
|
|
740
|
-
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
741
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.2s both' }}>
|
|
742
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Balance Over Time</div>
|
|
743
|
-
<div className="h-[120px] flex items-end gap-[2px]">
|
|
744
|
-
{(() => {
|
|
745
|
-
const recentHistory = balanceHistory.slice(-60)
|
|
746
|
-
const balances = recentHistory.map((snapshot) => Number.parseFloat(formatWalletAmount(selectedWallet.chain, getWalletBalanceAtomic(snapshot), { maxFractionDigits: 6 })) || 0)
|
|
747
|
-
const max = Math.max(...balances, 1)
|
|
748
|
-
return recentHistory.map((s, i) => (
|
|
749
|
-
<div
|
|
750
|
-
key={s.id || i}
|
|
751
|
-
className="flex-1 bg-accent/40 rounded-t-[2px] min-w-[3px] transition-all hover:bg-accent hover:scale-y-110"
|
|
752
|
-
style={{ height: `${Math.max(2, ((balances[i] || 0) / max) * 100)}%`, transitionDelay: `${i * 10}ms` }}
|
|
753
|
-
title={`${formatWalletAmount(selectedWallet.chain, getWalletBalanceAtomic(s), { minFractionDigits: 4, maxFractionDigits: 6 })} ${selectedWalletSymbol} — ${new Date(s.timestamp).toLocaleString()}`}
|
|
754
|
-
/>
|
|
755
|
-
))
|
|
756
|
-
})()}
|
|
757
|
-
</div>
|
|
758
|
-
</div>
|
|
759
|
-
)}
|
|
760
|
-
|
|
761
|
-
{/* Spending config */}
|
|
762
|
-
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
763
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.25s both' }}>
|
|
764
|
-
<div className="flex items-center justify-between mb-3">
|
|
765
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600">Spending Limits</div>
|
|
766
|
-
{!editingLimits && (
|
|
767
|
-
<button
|
|
768
|
-
type="button"
|
|
769
|
-
onClick={() => setEditingLimits(true)}
|
|
770
|
-
className="text-[11px] text-accent-bright hover:underline cursor-pointer"
|
|
771
|
-
style={{ fontFamily: 'inherit' }}
|
|
772
|
-
>
|
|
773
|
-
Edit
|
|
774
|
-
</button>
|
|
775
|
-
)}
|
|
776
|
-
</div>
|
|
777
|
-
|
|
778
|
-
{editingLimits ? (
|
|
779
|
-
<div className="space-y-3" style={{ animation: 'fade-in 0.3s ease' }}>
|
|
780
|
-
<div>
|
|
781
|
-
<label className="block text-[11px] text-text-3/70 mb-1">Per-transaction limit ({selectedWalletSymbol})</label>
|
|
782
|
-
<input
|
|
783
|
-
type="number"
|
|
784
|
-
step="0.01"
|
|
785
|
-
value={perTxLimit}
|
|
786
|
-
onChange={(e) => setPerTxLimit(e.target.value)}
|
|
787
|
-
className="w-full px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
|
|
788
|
-
style={{ fontFamily: 'inherit' }}
|
|
789
|
-
/>
|
|
790
|
-
</div>
|
|
791
|
-
<div>
|
|
792
|
-
<label className="block text-[11px] text-text-3/70 mb-1">Daily limit ({selectedWalletSymbol})</label>
|
|
793
|
-
<input
|
|
794
|
-
type="number"
|
|
795
|
-
step="0.1"
|
|
796
|
-
value={dailyLimit}
|
|
797
|
-
onChange={(e) => setDailyLimit(e.target.value)}
|
|
798
|
-
className="w-full px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
|
|
799
|
-
style={{ fontFamily: 'inherit' }}
|
|
800
|
-
/>
|
|
801
|
-
</div>
|
|
802
|
-
<div className="flex items-center gap-2">
|
|
803
|
-
<button
|
|
804
|
-
type="button"
|
|
805
|
-
onClick={() => setRequireApproval(!requireApproval)}
|
|
806
|
-
className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${requireApproval ? 'bg-accent' : 'bg-white/[0.12]'}`}
|
|
807
|
-
>
|
|
808
|
-
<span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${requireApproval ? 'translate-x-[18px]' : ''}`} />
|
|
809
|
-
</button>
|
|
810
|
-
<span className="text-[11px] text-text-3">Require approval for sends</span>
|
|
811
|
-
</div>
|
|
812
|
-
{!walletApprovalsEnabled && (
|
|
813
|
-
<p className="text-[10px] text-amber-300/80">
|
|
814
|
-
Global wallet approvals are currently off in Settings, so this per-wallet toggle is ignored until they are turned back on.
|
|
815
|
-
</p>
|
|
816
|
-
)}
|
|
817
|
-
<div className="flex gap-2 pt-1">
|
|
818
|
-
<button
|
|
819
|
-
type="button"
|
|
820
|
-
onClick={saveLimits}
|
|
821
|
-
disabled={saving}
|
|
822
|
-
className="px-3 py-1.5 rounded-[8px] bg-accent text-white text-[11px] font-600 hover:brightness-110 cursor-pointer disabled:opacity-50"
|
|
823
|
-
style={{ fontFamily: 'inherit' }}
|
|
824
|
-
>
|
|
825
|
-
{saving ? 'Saving...' : 'Save'}
|
|
826
|
-
</button>
|
|
827
|
-
<button
|
|
828
|
-
type="button"
|
|
829
|
-
onClick={() => setEditingLimits(false)}
|
|
830
|
-
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] text-text-3 text-[11px] hover:text-text-2 cursor-pointer"
|
|
831
|
-
style={{ fontFamily: 'inherit' }}
|
|
832
|
-
>
|
|
833
|
-
Cancel
|
|
834
|
-
</button>
|
|
835
|
-
</div>
|
|
836
|
-
</div>
|
|
837
|
-
) : (
|
|
838
|
-
<div className="space-y-2 text-[12px]">
|
|
839
|
-
<div className="flex justify-between">
|
|
840
|
-
<span className="text-text-3/70">Per-transaction</span>
|
|
841
|
-
<span className="text-text-2">{formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'perTx'), { maxFractionDigits: 6 })} {selectedWalletSymbol}</span>
|
|
842
|
-
</div>
|
|
843
|
-
<div className="flex justify-between">
|
|
844
|
-
<span className="text-text-3/70">Daily rolling</span>
|
|
845
|
-
<span className="text-text-2">{formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'daily'), { maxFractionDigits: 6 })} {selectedWalletSymbol}</span>
|
|
846
|
-
</div>
|
|
847
|
-
<div className="flex justify-between">
|
|
848
|
-
<span className="text-text-3/70">Approval</span>
|
|
849
|
-
<span className="text-text-2">
|
|
850
|
-
{!walletApprovalsEnabled
|
|
851
|
-
? 'Disabled globally'
|
|
852
|
-
: (selectedWallet.requireApproval ? 'Required' : 'Auto-send')}
|
|
853
|
-
</span>
|
|
854
|
-
</div>
|
|
855
|
-
</div>
|
|
856
|
-
)}
|
|
857
|
-
</div>
|
|
858
|
-
|
|
859
|
-
{/* Transaction history */}
|
|
860
|
-
<div style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.3s both' }}>
|
|
861
|
-
<div className="flex items-center justify-between gap-3 mb-3">
|
|
862
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600">Transactions</div>
|
|
863
|
-
<div className="text-[10px] text-text-3/45">
|
|
864
|
-
{filteredTransactions.length}{filteredTransactions.length !== transactions.length ? ` / ${transactions.length}` : ''} shown
|
|
865
|
-
</div>
|
|
866
|
-
</div>
|
|
867
|
-
<div className="flex flex-col gap-2 sm:flex-row sm:items-center mb-3">
|
|
868
|
-
<input
|
|
869
|
-
type="text"
|
|
870
|
-
value={transactionQuery}
|
|
871
|
-
onChange={(e) => setTransactionQuery(e.target.value)}
|
|
872
|
-
placeholder="Search memo, hash, address..."
|
|
873
|
-
className="flex-1 px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
|
|
874
|
-
style={{ fontFamily: 'inherit' }}
|
|
875
|
-
/>
|
|
876
|
-
<select
|
|
877
|
-
value={transactionFilter}
|
|
878
|
-
onChange={(e) => setTransactionFilter(e.target.value as WalletTransactionFilter)}
|
|
879
|
-
className="px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
|
|
880
|
-
style={{ fontFamily: 'inherit' }}
|
|
881
|
-
>
|
|
882
|
-
<option value="all">All</option>
|
|
883
|
-
<option value="confirmed">Confirmed</option>
|
|
884
|
-
<option value="pending">Pending</option>
|
|
885
|
-
<option value="failed">Failed</option>
|
|
886
|
-
<option value="send">Sends</option>
|
|
887
|
-
<option value="receive">Receives</option>
|
|
888
|
-
<option value="swap">Swaps</option>
|
|
889
|
-
</select>
|
|
890
|
-
</div>
|
|
891
|
-
{transactionsLoading && transactions.length === 0 ? (
|
|
892
|
-
<p className="text-[12px] text-text-3/50">Loading transactions...</p>
|
|
893
|
-
) : transactions.length === 0 ? (
|
|
894
|
-
<p className="text-[12px] text-text-3/50">No transactions yet.</p>
|
|
895
|
-
) : filteredTransactions.length === 0 ? (
|
|
896
|
-
<p className="text-[12px] text-text-3/50">No matching transactions.</p>
|
|
897
|
-
) : (
|
|
898
|
-
<div className="max-h-[420px] overflow-y-auto pr-1 space-y-2">
|
|
899
|
-
{filteredTransactions.map((tx, idx) => (
|
|
900
|
-
<div key={tx.id} className="flex items-center gap-3 p-3 rounded-[10px] border border-white/[0.06] bg-surface-2/30 transition-all hover:bg-surface-2/50"
|
|
901
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) both', animationDelay: `${0.35 + idx * 0.03}s` }}>
|
|
902
|
-
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-[12px] ${
|
|
903
|
-
tx.type === 'send' ? 'bg-red-500/15 text-red-400' :
|
|
904
|
-
tx.type === 'receive' ? 'bg-green-500/15 text-green-400' :
|
|
905
|
-
'bg-blue-500/15 text-blue-400'
|
|
906
|
-
}`}>
|
|
907
|
-
{tx.type === 'send' ? '\u2191' : tx.type === 'receive' ? '\u2193' : '\u21C4'}
|
|
908
|
-
</div>
|
|
909
|
-
<div className="flex-1 min-w-0">
|
|
910
|
-
<div className="flex items-center gap-2">
|
|
911
|
-
<span className="text-[12px] font-600 text-text-1">
|
|
912
|
-
{tx.type === 'send' ? '-' : '+'}{formatWalletAmount(tx.chain, getWalletAtomicAmount(tx), { minFractionDigits: 4, maxFractionDigits: 6 })} {getWalletAssetSymbol(tx.chain)}
|
|
913
|
-
</span>
|
|
914
|
-
<span className={`px-1.5 py-0.5 rounded-[4px] text-[9px] font-600 uppercase ${
|
|
915
|
-
getWalletTransactionStatusGroup(tx.status) === 'confirmed' ? 'bg-green-500/15 text-green-400' :
|
|
916
|
-
getWalletTransactionStatusGroup(tx.status) === 'pending' ? 'bg-amber-500/15 text-amber-400 animate-pulse' :
|
|
917
|
-
'bg-blue-500/15 text-blue-400'
|
|
918
|
-
}`}>
|
|
919
|
-
{tx.status.replace('_', ' ')}
|
|
920
|
-
</span>
|
|
921
|
-
<span className="px-1.5 py-0.5 rounded-[4px] bg-white/[0.05] text-[9px] font-600 uppercase text-text-3/70">
|
|
922
|
-
{tx.type}
|
|
923
|
-
</span>
|
|
924
|
-
</div>
|
|
925
|
-
<div className="text-[10px] text-text-3/50 font-mono truncate mt-0.5">
|
|
926
|
-
{tx.type === 'send' ? `To: ${tx.toAddress.slice(0, 8)}...${tx.toAddress.slice(-4)}` : `From: ${tx.fromAddress.slice(0, 8)}...${tx.fromAddress.slice(-4)}`}
|
|
927
|
-
</div>
|
|
928
|
-
{tx.memo && <div className="text-[10px] text-text-3/60 mt-0.5 truncate">{tx.memo}</div>}
|
|
929
|
-
<div className="text-[10px] text-text-3/40 font-mono truncate mt-0.5">
|
|
930
|
-
{tx.signature.slice(0, 10)}...{tx.signature.slice(-6)}
|
|
931
|
-
</div>
|
|
932
|
-
</div>
|
|
933
|
-
<div className="text-[10px] text-text-3/40 shrink-0">
|
|
934
|
-
{new Date(tx.timestamp).toLocaleDateString()}
|
|
935
|
-
</div>
|
|
936
|
-
{tx.status === 'pending_approval' && (
|
|
937
|
-
<button
|
|
938
|
-
type="button"
|
|
939
|
-
onClick={() => setPendingApproval(tx)}
|
|
940
|
-
className="shrink-0 px-2 py-1 rounded-[6px] bg-amber-500/15 text-amber-400 text-[10px] font-600 hover:bg-amber-500/25 cursor-pointer transition-all hover:scale-[1.05]"
|
|
941
|
-
style={{ fontFamily: 'inherit', animation: 'spring-in 0.4s var(--ease-spring)' }}
|
|
942
|
-
>
|
|
943
|
-
Review
|
|
944
|
-
</button>
|
|
945
|
-
)}
|
|
946
|
-
</div>
|
|
947
|
-
))}
|
|
948
|
-
</div>
|
|
949
|
-
)}
|
|
950
|
-
</div>
|
|
951
|
-
|
|
952
|
-
{/* Danger zone */}
|
|
953
|
-
<div className="p-4 rounded-[14px] border border-red-500/15 bg-red-500/5"
|
|
954
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.4s both' }}>
|
|
955
|
-
<div className="text-[11px] text-red-400/80 uppercase tracking-wide font-600 mb-2">Danger Zone</div>
|
|
956
|
-
{confirmDelete ? (
|
|
957
|
-
<div className="space-y-2" style={{ animation: 'spring-in 0.3s var(--ease-spring)' }}>
|
|
958
|
-
<p className="text-[11px] text-text-3/70">
|
|
959
|
-
This will permanently delete the wallet and its private key. Any remaining balance will be inaccessible. This cannot be undone.
|
|
960
|
-
</p>
|
|
961
|
-
<div className="flex gap-2">
|
|
962
|
-
<button
|
|
963
|
-
type="button"
|
|
964
|
-
onClick={handleDelete}
|
|
965
|
-
disabled={deleting}
|
|
966
|
-
className="px-3 py-1.5 rounded-[8px] bg-red-500 text-white text-[11px] font-600 hover:brightness-110 cursor-pointer disabled:opacity-50"
|
|
967
|
-
style={{ fontFamily: 'inherit' }}
|
|
968
|
-
>
|
|
969
|
-
{deleting ? 'Deleting...' : 'Confirm Delete'}
|
|
970
|
-
</button>
|
|
971
|
-
<button
|
|
972
|
-
type="button"
|
|
973
|
-
onClick={() => setConfirmDelete(false)}
|
|
974
|
-
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] text-text-3 text-[11px] hover:text-text-2 cursor-pointer"
|
|
975
|
-
style={{ fontFamily: 'inherit' }}
|
|
976
|
-
>
|
|
977
|
-
Cancel
|
|
978
|
-
</button>
|
|
979
|
-
</div>
|
|
980
|
-
</div>
|
|
981
|
-
) : (
|
|
982
|
-
<button
|
|
983
|
-
type="button"
|
|
984
|
-
onClick={() => setConfirmDelete(true)}
|
|
985
|
-
className="px-3 py-1.5 rounded-[8px] border border-red-500/30 text-red-400 text-[11px] font-600 hover:bg-red-500/10 cursor-pointer transition-colors"
|
|
986
|
-
style={{ fontFamily: 'inherit' }}
|
|
987
|
-
>
|
|
988
|
-
Delete Wallet
|
|
989
|
-
</button>
|
|
990
|
-
)}
|
|
991
|
-
</div>
|
|
992
|
-
</div>
|
|
993
|
-
) : (
|
|
994
|
-
<div className="flex-1 flex items-center justify-center">
|
|
995
|
-
<p className="text-[12px] text-text-3/50" style={{ animation: 'fade-in 1s ease' }}>Select a wallet to view details</p>
|
|
996
|
-
</div>
|
|
997
|
-
)}
|
|
998
|
-
|
|
999
|
-
{/* Approval dialog */}
|
|
1000
|
-
{pendingApproval && selectedWallet && (
|
|
1001
|
-
<WalletApprovalDialog
|
|
1002
|
-
transaction={pendingApproval}
|
|
1003
|
-
walletAddress={selectedWallet.publicKey}
|
|
1004
|
-
onClose={() => setPendingApproval(null)}
|
|
1005
|
-
onResolved={loadDetail}
|
|
1006
|
-
/>
|
|
1007
|
-
)}
|
|
1008
|
-
</div>
|
|
1009
|
-
)
|
|
1010
|
-
}
|