@swarmclawai/swarmclaw 0.7.7 → 0.8.0
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 +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
5
|
import { copyTextToClipboard } from '@/lib/clipboard'
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
@@ -8,9 +8,47 @@ import { useWs } from '@/hooks/use-ws'
|
|
|
8
8
|
import { WalletApprovalDialog } from './wallet-approval-dialog'
|
|
9
9
|
import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
10
10
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
11
|
-
import type { AgentWallet, WalletTransaction, WalletBalanceSnapshot, Agent } from '@/types'
|
|
11
|
+
import type { AgentWallet, WalletTransaction, WalletBalanceSnapshot, WalletAssetBalance, WalletPortfolioSummary, Agent, WalletChain } from '@/types'
|
|
12
|
+
import {
|
|
13
|
+
SUPPORTED_WALLET_CHAINS,
|
|
14
|
+
formatWalletAmount,
|
|
15
|
+
getWalletAssetSymbol,
|
|
16
|
+
getWalletAtomicAmount,
|
|
17
|
+
getWalletBalanceAtomic,
|
|
18
|
+
getWalletChainMeta,
|
|
19
|
+
getWalletLimitAtomic,
|
|
20
|
+
parseDisplayAmountToAtomic,
|
|
21
|
+
} from '@/lib/wallet'
|
|
22
|
+
import { type WalletTransactionFilter, filterWalletTransactions, getWalletTransactionStatusGroup } from '@/lib/wallet-transactions'
|
|
23
|
+
import { toast } from 'sonner'
|
|
24
|
+
|
|
25
|
+
type SafeWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & {
|
|
26
|
+
balanceAtomic?: string
|
|
27
|
+
balanceLamports?: number
|
|
28
|
+
balanceFormatted?: string
|
|
29
|
+
balanceSymbol?: string
|
|
30
|
+
assets?: WalletAssetBalance[]
|
|
31
|
+
portfolioSummary?: WalletPortfolioSummary
|
|
32
|
+
isActive?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getAgentWalletIds(agent: Agent | undefined | null): string[] {
|
|
36
|
+
const ids = Array.isArray(agent?.walletIds)
|
|
37
|
+
? agent.walletIds.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
38
|
+
: []
|
|
39
|
+
const legacy = typeof agent?.walletId === 'string' && agent.walletId.trim()
|
|
40
|
+
? [agent.walletId.trim()]
|
|
41
|
+
: []
|
|
42
|
+
return [...new Set([...ids, ...legacy])]
|
|
43
|
+
}
|
|
12
44
|
|
|
13
|
-
|
|
45
|
+
function getAgentActiveWalletId(agent: Agent | undefined | null, fallbackWallets: SafeWallet[] = []): string | null {
|
|
46
|
+
const walletIds = getAgentWalletIds(agent)
|
|
47
|
+
if (typeof agent?.activeWalletId === 'string' && walletIds.includes(agent.activeWalletId)) return agent.activeWalletId
|
|
48
|
+
if (typeof agent?.walletId === 'string' && walletIds.includes(agent.walletId)) return agent.walletId
|
|
49
|
+
const activeWallet = fallbackWallets.find((wallet) => wallet.isActive)
|
|
50
|
+
return activeWallet?.id || fallbackWallets[0]?.id || walletIds[0] || null
|
|
51
|
+
}
|
|
14
52
|
|
|
15
53
|
function SolanaIcon({ size = 12, className = '', shimmer = false }: { size?: number; className?: string; shimmer?: boolean }) {
|
|
16
54
|
return (
|
|
@@ -33,6 +71,45 @@ function SolanaIcon({ size = 12, className = '', shimmer = false }: { size?: num
|
|
|
33
71
|
)
|
|
34
72
|
}
|
|
35
73
|
|
|
74
|
+
function EthereumIcon({ size = 12, className = '', shimmer = false }: { size?: number; className?: string; shimmer?: boolean }) {
|
|
75
|
+
return (
|
|
76
|
+
<div className={`relative flex items-center justify-center ${className}`}>
|
|
77
|
+
<svg width={size} height={size} viewBox="0 0 256 417" className="relative z-10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
78
|
+
<path d="M127.6 0L124.8 9.5V279.1L127.6 281.9L255.2 208.3L127.6 0Z" fill="#8A92B2" />
|
|
79
|
+
<path d="M127.6 0L0 208.3L127.6 281.9V151.1V0Z" fill="#62688F" />
|
|
80
|
+
<path d="M127.6 306.1L126 308V416.9L127.6 421.6L255.3 232.6L127.6 306.1Z" fill="#8A92B2" />
|
|
81
|
+
<path d="M127.6 421.6V306.1L0 232.6L127.6 421.6Z" fill="#62688F" />
|
|
82
|
+
<path d="M127.6 281.9L255.2 208.3L127.6 151.1V281.9Z" fill="#454A75" />
|
|
83
|
+
<path d="M0 208.3L127.6 281.9V151.1L0 208.3Z" fill="#8A92B2" />
|
|
84
|
+
</svg>
|
|
85
|
+
{shimmer && (
|
|
86
|
+
<div className="absolute inset-0 bg-sky-400/20 blur-md rounded-full animate-pulse" />
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ChainIcon({ chain, size = 12, className = '', shimmer = false }: { chain: WalletChain; size?: number; className?: string; shimmer?: boolean }) {
|
|
93
|
+
if (chain === 'ethereum') return <EthereumIcon size={size} className={className} shimmer={shimmer} />
|
|
94
|
+
return <SolanaIcon size={size} className={className} shimmer={shimmer} />
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function walletBalanceLabel(wallet: SafeWallet): string {
|
|
98
|
+
return wallet.balanceFormatted || formatWalletAmount(wallet.chain, getWalletBalanceAtomic(wallet), { minFractionDigits: 3, maxFractionDigits: 6 })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function walletAssetCountLabel(wallet: SafeWallet): string | null {
|
|
102
|
+
const count = wallet.portfolioSummary?.nonZeroAssets
|
|
103
|
+
if (!count) return null
|
|
104
|
+
return `${count} asset${count === 1 ? '' : 's'}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function suggestCreateChain(wallets: SafeWallet[], agentId?: string | null): WalletChain {
|
|
108
|
+
if (!agentId) return 'solana'
|
|
109
|
+
const connectedChains = new Set(wallets.filter((wallet) => wallet.agentId === agentId).map((wallet) => wallet.chain))
|
|
110
|
+
return SUPPORTED_WALLET_CHAINS.find((chain) => !connectedChains.has(chain)) || 'solana'
|
|
111
|
+
}
|
|
112
|
+
|
|
36
113
|
export function WalletPanel() {
|
|
37
114
|
const agents = useAppStore((s) => s.agents)
|
|
38
115
|
const walletPanelAgentId = useAppStore((s) => s.walletPanelAgentId)
|
|
@@ -45,7 +122,11 @@ export function WalletPanel() {
|
|
|
45
122
|
const [transactions, setTransactions] = useState<WalletTransaction[]>([])
|
|
46
123
|
const [balanceHistory, setBalanceHistory] = useState<WalletBalanceSnapshot[]>([])
|
|
47
124
|
const [loading, setLoading] = useState(true)
|
|
125
|
+
const [transactionsLoading, setTransactionsLoading] = useState(false)
|
|
48
126
|
const [pendingApproval, setPendingApproval] = useState<WalletTransaction | null>(null)
|
|
127
|
+
const [transactionFilter, setTransactionFilter] = useState<WalletTransactionFilter>('all')
|
|
128
|
+
const [transactionQuery, setTransactionQuery] = useState('')
|
|
129
|
+
const detailRequestRef = useRef(0)
|
|
49
130
|
|
|
50
131
|
// Settings edit state
|
|
51
132
|
const [editingLimits, setEditingLimits] = useState(false)
|
|
@@ -55,6 +136,7 @@ export function WalletPanel() {
|
|
|
55
136
|
const [saving, setSaving] = useState(false)
|
|
56
137
|
const [deleting, setDeleting] = useState(false)
|
|
57
138
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
139
|
+
const [settingDefault, setSettingDefault] = useState(false)
|
|
58
140
|
const [reassigning, setReassigning] = useState(false)
|
|
59
141
|
const [reassignSaving, setReassignSaving] = useState(false)
|
|
60
142
|
const [reassignError, setReassignError] = useState('')
|
|
@@ -62,6 +144,7 @@ export function WalletPanel() {
|
|
|
62
144
|
// Create wallet state
|
|
63
145
|
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
64
146
|
const [createAgentId, setCreateAgentId] = useState('')
|
|
147
|
+
const [createChain, setCreateChain] = useState<WalletChain>('solana')
|
|
65
148
|
const [creating, setCreating] = useState(false)
|
|
66
149
|
const [createError, setCreateError] = useState('')
|
|
67
150
|
|
|
@@ -71,7 +154,8 @@ export function WalletPanel() {
|
|
|
71
154
|
setWallets(data)
|
|
72
155
|
|
|
73
156
|
if (!walletPanelAgentId && !selectedWalletId && Object.keys(data).length > 0) {
|
|
74
|
-
|
|
157
|
+
const defaultWallet = Object.values(data).find((wallet) => wallet.isActive) || Object.values(data)[0]
|
|
158
|
+
if (defaultWallet) setSelectedWalletId(defaultWallet.id)
|
|
75
159
|
}
|
|
76
160
|
} catch { /* ignore */ }
|
|
77
161
|
setLoading(false)
|
|
@@ -79,70 +163,116 @@ export function WalletPanel() {
|
|
|
79
163
|
}, [walletPanelAgentId])
|
|
80
164
|
|
|
81
165
|
useEffect(() => { loadWallets() }, [loadWallets])
|
|
82
|
-
useWs('wallets', loadWallets, 15000)
|
|
83
166
|
|
|
167
|
+
// Sync wallet selection when agent panel changes
|
|
84
168
|
useEffect(() => {
|
|
85
169
|
if (!walletPanelAgentId) return
|
|
86
|
-
const
|
|
170
|
+
const agentWallets = Object.values(wallets).filter((wallet) => wallet.agentId === walletPanelAgentId)
|
|
171
|
+
const selectedAgentWallet = selectedWalletId
|
|
172
|
+
? agentWallets.find((wallet) => wallet.id === selectedWalletId) || null
|
|
173
|
+
: null
|
|
174
|
+
if (selectedAgentWallet) {
|
|
175
|
+
setShowCreateForm(false)
|
|
176
|
+
setCreateError('')
|
|
177
|
+
setCreateAgentId(walletPanelAgentId)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
const activeWalletId = getAgentActiveWalletId(agents[walletPanelAgentId] as Agent | undefined, agentWallets)
|
|
181
|
+
const match = agentWallets.find((wallet) => wallet.id === activeWalletId) || agentWallets[0]
|
|
87
182
|
if (match) {
|
|
88
183
|
setSelectedWalletId(match.id)
|
|
89
184
|
setShowCreateForm(false)
|
|
90
185
|
setCreateError('')
|
|
186
|
+
setCreateAgentId(walletPanelAgentId)
|
|
91
187
|
return
|
|
92
188
|
}
|
|
93
189
|
if (!agents[walletPanelAgentId]) return
|
|
94
190
|
setSelectedWalletId(null)
|
|
95
191
|
setShowCreateForm(true)
|
|
96
192
|
setCreateAgentId(walletPanelAgentId)
|
|
193
|
+
setCreateChain(suggestCreateChain(Object.values(wallets), walletPanelAgentId))
|
|
97
194
|
setCreateError('')
|
|
98
|
-
}, [agents, walletPanelAgentId, wallets])
|
|
195
|
+
}, [agents, selectedWalletId, walletPanelAgentId, wallets])
|
|
99
196
|
|
|
100
197
|
// Load detail when wallet selected
|
|
101
198
|
const selectedWallet = selectedWalletId ? wallets[selectedWalletId] : null
|
|
102
199
|
|
|
103
|
-
const loadDetail = useCallback(async () => {
|
|
104
|
-
if (!
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
])
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
200
|
+
const loadDetail = useCallback(async (walletId = selectedWalletId) => {
|
|
201
|
+
if (!walletId) return
|
|
202
|
+
const requestId = ++detailRequestRef.current
|
|
203
|
+
setTransactionsLoading(true)
|
|
204
|
+
const [detailResult, txResult, historyResult] = await Promise.allSettled([
|
|
205
|
+
api<SafeWallet>('GET', `/wallets/${walletId}`),
|
|
206
|
+
api<WalletTransaction[]>('GET', `/wallets/${walletId}/transactions`),
|
|
207
|
+
api<WalletBalanceSnapshot[]>('GET', `/wallets/${walletId}/balance-history`),
|
|
208
|
+
])
|
|
209
|
+
if (detailRequestRef.current !== requestId) return
|
|
210
|
+
|
|
211
|
+
if (detailResult.status === 'fulfilled') {
|
|
212
|
+
setWallets((prev) => ({ ...prev, [walletId]: detailResult.value }))
|
|
213
|
+
}
|
|
214
|
+
if (txResult.status === 'fulfilled') {
|
|
215
|
+
setTransactions(txResult.value)
|
|
216
|
+
const pending = txResult.value.find((tx) => tx.status === 'pending_approval')
|
|
217
|
+
setPendingApproval(pending || null)
|
|
218
|
+
}
|
|
219
|
+
if (historyResult.status === 'fulfilled') {
|
|
220
|
+
setBalanceHistory(historyResult.value)
|
|
221
|
+
}
|
|
222
|
+
setTransactionsLoading(false)
|
|
119
223
|
}, [selectedWalletId])
|
|
120
224
|
|
|
121
225
|
useEffect(() => { loadDetail() }, [loadDetail])
|
|
122
226
|
|
|
227
|
+
const refreshWalletData = useCallback(async () => {
|
|
228
|
+
await loadWallets()
|
|
229
|
+
if (selectedWalletId) {
|
|
230
|
+
await loadDetail(selectedWalletId)
|
|
231
|
+
}
|
|
232
|
+
}, [loadDetail, loadWallets, selectedWalletId])
|
|
233
|
+
|
|
234
|
+
useWs('wallets', refreshWalletData, 15000)
|
|
235
|
+
|
|
123
236
|
// Initialize limits when wallet selected
|
|
124
237
|
useEffect(() => {
|
|
125
238
|
if (selectedWallet) {
|
|
126
|
-
setPerTxLimit(
|
|
127
|
-
setDailyLimit(
|
|
239
|
+
setPerTxLimit(formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'perTx'), { maxFractionDigits: 6 }))
|
|
240
|
+
setDailyLimit(formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'daily'), { maxFractionDigits: 6 }))
|
|
128
241
|
setRequireApproval(selectedWallet.requireApproval)
|
|
129
242
|
}
|
|
130
243
|
}, [selectedWallet])
|
|
131
244
|
|
|
132
245
|
const saveLimits = useCallback(async () => {
|
|
133
|
-
if (!selectedWalletId) return
|
|
246
|
+
if (!selectedWalletId || !selectedWallet) return
|
|
134
247
|
setSaving(true)
|
|
135
248
|
try {
|
|
249
|
+
const spendingLimitAtomic = parseDisplayAmountToAtomic(perTxLimit || '0', getWalletChainMeta(selectedWallet.chain).decimals)
|
|
250
|
+
const dailyLimitAtomic = parseDisplayAmountToAtomic(dailyLimit || '0', getWalletChainMeta(selectedWallet.chain).decimals)
|
|
136
251
|
await api('PATCH', `/wallets/${selectedWalletId}`, {
|
|
137
|
-
|
|
138
|
-
|
|
252
|
+
spendingLimitAtomic,
|
|
253
|
+
dailyLimitAtomic,
|
|
139
254
|
requireApproval,
|
|
140
255
|
})
|
|
141
256
|
setEditingLimits(false)
|
|
142
257
|
loadDetail()
|
|
143
|
-
} catch
|
|
258
|
+
} catch (err: unknown) {
|
|
259
|
+
toast.error(err instanceof Error ? err.message : String(err))
|
|
260
|
+
}
|
|
144
261
|
setSaving(false)
|
|
145
|
-
}, [selectedWalletId, perTxLimit, dailyLimit, requireApproval, loadDetail])
|
|
262
|
+
}, [selectedWalletId, selectedWallet, perTxLimit, dailyLimit, requireApproval, loadDetail])
|
|
263
|
+
|
|
264
|
+
const setDefaultWallet = useCallback(async () => {
|
|
265
|
+
if (!selectedWalletId) return
|
|
266
|
+
setSettingDefault(true)
|
|
267
|
+
try {
|
|
268
|
+
await api('PATCH', `/wallets/${selectedWalletId}`, { makeActive: true })
|
|
269
|
+
toast.success('Default wallet updated')
|
|
270
|
+
loadWallets()
|
|
271
|
+
} catch (err: unknown) {
|
|
272
|
+
toast.error(err instanceof Error ? err.message : String(err))
|
|
273
|
+
}
|
|
274
|
+
setSettingDefault(false)
|
|
275
|
+
}, [selectedWalletId, loadWallets])
|
|
146
276
|
|
|
147
277
|
const handleDelete = useCallback(async () => {
|
|
148
278
|
if (!selectedWalletId) return
|
|
@@ -165,25 +295,42 @@ export function WalletPanel() {
|
|
|
165
295
|
setTimeout(() => setCopied(false), 2000)
|
|
166
296
|
}, [selectedWallet])
|
|
167
297
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
298
|
+
const agentsMissingSelectedChain = useMemo(() => {
|
|
299
|
+
return Object.values(agents).filter((agent) => !Object.values(wallets).some((wallet) => wallet.agentId === agent.id && wallet.chain === createChain)) as Agent[]
|
|
300
|
+
}, [agents, createChain, wallets])
|
|
301
|
+
|
|
302
|
+
const canCreateMoreWallets = useMemo(() => {
|
|
303
|
+
return Object.values(agents).some((agent) =>
|
|
304
|
+
SUPPORTED_WALLET_CHAINS.some((chain) => !Object.values(wallets).some((wallet) => wallet.agentId === agent.id && wallet.chain === chain)),
|
|
305
|
+
)
|
|
171
306
|
}, [agents, wallets])
|
|
172
307
|
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (!createAgentId) return
|
|
310
|
+
if (agentsMissingSelectedChain.some((agent) => agent.id === createAgentId)) return
|
|
311
|
+
setCreateAgentId('')
|
|
312
|
+
}, [agentsMissingSelectedChain, createAgentId])
|
|
313
|
+
|
|
173
314
|
const createWallet = useCallback(async () => {
|
|
174
315
|
if (!createAgentId) return
|
|
175
316
|
setCreating(true)
|
|
176
317
|
setCreateError('')
|
|
177
318
|
try {
|
|
178
|
-
await api('POST', '/wallets', { agentId: createAgentId })
|
|
319
|
+
await api('POST', '/wallets', { agentId: createAgentId, chain: createChain })
|
|
179
320
|
setShowCreateForm(false)
|
|
180
321
|
setCreateAgentId('')
|
|
322
|
+
setCreateChain('solana')
|
|
181
323
|
loadWallets()
|
|
182
324
|
} catch (err: unknown) {
|
|
183
325
|
setCreateError(err instanceof Error ? err.message : String(err))
|
|
184
326
|
}
|
|
185
327
|
setCreating(false)
|
|
186
|
-
}, [createAgentId, loadWallets])
|
|
328
|
+
}, [createAgentId, createChain, loadWallets])
|
|
329
|
+
|
|
330
|
+
const filteredTransactions = useMemo(
|
|
331
|
+
() => filterWalletTransactions(transactions, { filter: transactionFilter, query: transactionQuery }),
|
|
332
|
+
[transactionFilter, transactionQuery, transactions],
|
|
333
|
+
)
|
|
187
334
|
|
|
188
335
|
if (loading) {
|
|
189
336
|
return (
|
|
@@ -193,7 +340,31 @@ export function WalletPanel() {
|
|
|
193
340
|
)
|
|
194
341
|
}
|
|
195
342
|
|
|
196
|
-
const walletList = Object.values(wallets)
|
|
343
|
+
const walletList = Object.values(wallets).sort((a, b) => {
|
|
344
|
+
const aAgent = agents[a.agentId] as Agent | undefined
|
|
345
|
+
const bAgent = agents[b.agentId] as Agent | undefined
|
|
346
|
+
const aActive = a.isActive === true || getAgentActiveWalletId(aAgent, [a]) === a.id
|
|
347
|
+
const bActive = b.isActive === true || getAgentActiveWalletId(bAgent, [b]) === b.id
|
|
348
|
+
if (a.agentId === b.agentId && aActive !== bActive) return aActive ? -1 : 1
|
|
349
|
+
const agentCompare = (aAgent?.name || a.agentId).localeCompare(bAgent?.name || b.agentId)
|
|
350
|
+
if (agentCompare !== 0) return agentCompare
|
|
351
|
+
return a.chain.localeCompare(b.chain)
|
|
352
|
+
})
|
|
353
|
+
const selectedWalletMeta = selectedWallet ? getWalletChainMeta(selectedWallet.chain) : null
|
|
354
|
+
const selectedWalletSymbol = selectedWallet ? getWalletAssetSymbol(selectedWallet.chain) : null
|
|
355
|
+
const selectedWalletBalance = selectedWallet ? walletBalanceLabel(selectedWallet) : null
|
|
356
|
+
const selectedWalletAssets = (selectedWallet?.assets || []).filter((asset) => BigInt(asset.balanceAtomic) > BigInt(0))
|
|
357
|
+
const selectedAgent = selectedWallet ? agents[selectedWallet.agentId] as Agent | undefined : undefined
|
|
358
|
+
const selectedAgentWallets = selectedWallet
|
|
359
|
+
? walletList.filter((wallet) => wallet.agentId === selectedWallet.agentId)
|
|
360
|
+
: []
|
|
361
|
+
const selectedAgentActiveWalletId = getAgentActiveWalletId(selectedAgent, selectedAgentWallets)
|
|
362
|
+
const reassignCandidates = selectedWallet
|
|
363
|
+
? (Object.values(agents).filter((agent) => (
|
|
364
|
+
agent.id !== selectedWallet.agentId
|
|
365
|
+
&& !walletList.some((wallet) => wallet.agentId === agent.id && wallet.chain === selectedWallet.chain)
|
|
366
|
+
)) as Agent[])
|
|
367
|
+
: []
|
|
197
368
|
|
|
198
369
|
if (walletList.length === 0) {
|
|
199
370
|
return (
|
|
@@ -203,14 +374,23 @@ export function WalletPanel() {
|
|
|
203
374
|
<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" />
|
|
204
375
|
</svg>
|
|
205
376
|
<h3 className="font-display text-[14px] font-600 text-text-2 mb-2">No wallets yet</h3>
|
|
206
|
-
{
|
|
377
|
+
{agentsMissingSelectedChain.length > 0 ? (
|
|
207
378
|
<div className="mt-4 space-y-3">
|
|
208
379
|
<AgentPickerList
|
|
209
|
-
agents={
|
|
380
|
+
agents={agentsMissingSelectedChain}
|
|
210
381
|
selected={createAgentId}
|
|
211
382
|
onSelect={(id) => setCreateAgentId(id === createAgentId ? '' : id)}
|
|
212
383
|
maxHeight={180}
|
|
213
384
|
/>
|
|
385
|
+
<select
|
|
386
|
+
value={createChain}
|
|
387
|
+
onChange={(e) => setCreateChain(e.target.value as WalletChain)}
|
|
388
|
+
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"
|
|
389
|
+
style={{ fontFamily: 'inherit' }}
|
|
390
|
+
>
|
|
391
|
+
<option value="solana">Solana</option>
|
|
392
|
+
<option value="ethereum">Ethereum (EVM)</option>
|
|
393
|
+
</select>
|
|
214
394
|
<button
|
|
215
395
|
type="button"
|
|
216
396
|
onClick={createWallet}
|
|
@@ -224,7 +404,7 @@ export function WalletPanel() {
|
|
|
224
404
|
</div>
|
|
225
405
|
) : (
|
|
226
406
|
<p className="text-[12px] text-text-3/60">
|
|
227
|
-
|
|
407
|
+
Every agent already has a {getWalletChainMeta(createChain).label} wallet.
|
|
228
408
|
</p>
|
|
229
409
|
)}
|
|
230
410
|
</div>
|
|
@@ -240,10 +420,15 @@ export function WalletPanel() {
|
|
|
240
420
|
<h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] flex-1">Wallets</h2>
|
|
241
421
|
<button
|
|
242
422
|
type="button"
|
|
243
|
-
onClick={() => {
|
|
244
|
-
|
|
423
|
+
onClick={() => {
|
|
424
|
+
setShowCreateForm(!showCreateForm)
|
|
425
|
+
setCreateAgentId(walletPanelAgentId || '')
|
|
426
|
+
setCreateChain(suggestCreateChain(walletList, walletPanelAgentId))
|
|
427
|
+
setCreateError('')
|
|
428
|
+
}}
|
|
429
|
+
disabled={!canCreateMoreWallets}
|
|
245
430
|
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"
|
|
246
|
-
title={
|
|
431
|
+
title={canCreateMoreWallets ? 'Create wallet' : 'Every agent already has both wallet types'}
|
|
247
432
|
>
|
|
248
433
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
249
434
|
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
|
|
@@ -255,11 +440,20 @@ export function WalletPanel() {
|
|
|
255
440
|
<div className="mx-1 mb-2 p-2.5 rounded-[8px] border border-accent/20 bg-accent-soft/10 space-y-2"
|
|
256
441
|
style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}>
|
|
257
442
|
<AgentPickerList
|
|
258
|
-
agents={
|
|
443
|
+
agents={agentsMissingSelectedChain}
|
|
259
444
|
selected={createAgentId}
|
|
260
445
|
onSelect={(id) => setCreateAgentId(id === createAgentId ? '' : id)}
|
|
261
446
|
maxHeight={160}
|
|
262
447
|
/>
|
|
448
|
+
<select
|
|
449
|
+
value={createChain}
|
|
450
|
+
onChange={(e) => setCreateChain(e.target.value as WalletChain)}
|
|
451
|
+
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"
|
|
452
|
+
style={{ fontFamily: 'inherit' }}
|
|
453
|
+
>
|
|
454
|
+
<option value="solana">Solana</option>
|
|
455
|
+
<option value="ethereum">Ethereum (EVM)</option>
|
|
456
|
+
</select>
|
|
263
457
|
<div className="flex gap-1.5">
|
|
264
458
|
<button
|
|
265
459
|
type="button"
|
|
@@ -284,6 +478,7 @@ export function WalletPanel() {
|
|
|
284
478
|
)}
|
|
285
479
|
{walletList.map((w, idx) => {
|
|
286
480
|
const a = agents[w.agentId] as Agent | undefined
|
|
481
|
+
const isActive = w.isActive === true || getAgentActiveWalletId(a, walletList.filter((wallet) => wallet.agentId === w.agentId)) === w.id
|
|
287
482
|
return (
|
|
288
483
|
<button
|
|
289
484
|
key={w.id}
|
|
@@ -298,13 +493,19 @@ export function WalletPanel() {
|
|
|
298
493
|
>
|
|
299
494
|
<AgentAvatar seed={a?.avatarSeed || null} avatarUrl={a?.avatarUrl} name={a?.name || '?'} size={28} />
|
|
300
495
|
<div className="flex-1 min-w-0">
|
|
301
|
-
<div className="
|
|
496
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
497
|
+
<div className="text-[12px] font-600 truncate">{a?.name || w.agentId}</div>
|
|
498
|
+
{isActive && (
|
|
499
|
+
<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">
|
|
500
|
+
Default
|
|
501
|
+
</span>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
302
504
|
<div className="text-[10px] text-text-3/50 font-mono truncate mt-0.5 flex items-center gap-1">
|
|
303
|
-
{w.chain
|
|
505
|
+
<ChainIcon chain={w.chain} size={9} className="shrink-0 opacity-50" />
|
|
304
506
|
<span className="truncate">{w.publicKey.slice(0, 8)}...{w.publicKey.slice(-4)}</span>
|
|
305
|
-
{
|
|
306
|
-
|
|
307
|
-
)}
|
|
507
|
+
<span className="text-text-3/40">{walletBalanceLabel(w)} {getWalletAssetSymbol(w.chain)}</span>
|
|
508
|
+
{walletAssetCountLabel(w) && <span className="text-text-3/35">{walletAssetCountLabel(w)}</span>}
|
|
308
509
|
</div>
|
|
309
510
|
</div>
|
|
310
511
|
</button>
|
|
@@ -328,6 +529,49 @@ export function WalletPanel() {
|
|
|
328
529
|
</p>
|
|
329
530
|
</div>
|
|
330
531
|
|
|
532
|
+
{selectedAgentWallets.length > 1 && (
|
|
533
|
+
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
534
|
+
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.03s both' }}>
|
|
535
|
+
<div className="flex items-center justify-between gap-3 mb-3">
|
|
536
|
+
<div>
|
|
537
|
+
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600">Combined Wallet Stats</div>
|
|
538
|
+
<p className="text-[12px] text-text-3/70 mt-1">
|
|
539
|
+
{selectedAgentWallets.length} wallets connected for this agent. Pick a wallet in the sidebar for chain-specific history and controls.
|
|
540
|
+
</p>
|
|
541
|
+
</div>
|
|
542
|
+
<div className="text-right">
|
|
543
|
+
<div className="text-[18px] font-600 text-text-1">{selectedAgentWallets.length}</div>
|
|
544
|
+
<div className="text-[10px] uppercase tracking-wide text-text-3/50">Wallets</div>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
<div className="grid gap-2 md:grid-cols-2">
|
|
548
|
+
{selectedAgentWallets.map((wallet) => (
|
|
549
|
+
<div key={wallet.id} className="rounded-[10px] border border-white/[0.06] bg-black/10 px-3 py-2">
|
|
550
|
+
<div className="flex items-center gap-2">
|
|
551
|
+
<span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">
|
|
552
|
+
{getWalletChainMeta(wallet.chain).label}
|
|
553
|
+
</span>
|
|
554
|
+
{(wallet.id === selectedAgentActiveWalletId || wallet.isActive) && (
|
|
555
|
+
<span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/40 text-accent-bright text-[8px] font-700 uppercase tracking-wide">
|
|
556
|
+
Default
|
|
557
|
+
</span>
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
<div className="mt-1 text-[14px] font-600 text-text-1">
|
|
561
|
+
{walletBalanceLabel(wallet)} {getWalletAssetSymbol(wallet.chain)}
|
|
562
|
+
</div>
|
|
563
|
+
{wallet.portfolioSummary?.nonZeroAssets ? (
|
|
564
|
+
<div className="mt-1 text-[10px] text-text-3/55">
|
|
565
|
+
{wallet.portfolioSummary.nonZeroAssets} detected asset{wallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'}
|
|
566
|
+
</div>
|
|
567
|
+
) : null}
|
|
568
|
+
<div className="mt-1 text-[10px] text-text-3/55 font-mono truncate">{wallet.publicKey}</div>
|
|
569
|
+
</div>
|
|
570
|
+
))}
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
)}
|
|
574
|
+
|
|
331
575
|
{/* Agent & Address */}
|
|
332
576
|
<div style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.05s both' }}>
|
|
333
577
|
<div className="flex items-center gap-2 mb-2">
|
|
@@ -349,9 +593,14 @@ export function WalletPanel() {
|
|
|
349
593
|
)
|
|
350
594
|
})()}
|
|
351
595
|
<span className="inline-flex items-center gap-1 text-[11px] text-text-3/40 uppercase tracking-wide font-600">
|
|
352
|
-
{selectedWallet.chain
|
|
596
|
+
<ChainIcon chain={selectedWallet.chain} size={11} />
|
|
353
597
|
{selectedWallet.chain}
|
|
354
598
|
</span>
|
|
599
|
+
{(selectedWallet.id === selectedAgentActiveWalletId || selectedWallet.isActive) && (
|
|
600
|
+
<span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/40 text-accent-bright text-[9px] font-700 uppercase tracking-wide">
|
|
601
|
+
Default
|
|
602
|
+
</span>
|
|
603
|
+
)}
|
|
355
604
|
<button
|
|
356
605
|
type="button"
|
|
357
606
|
onClick={() => { setReassigning(!reassigning); setReassignError('') }}
|
|
@@ -360,27 +609,44 @@ export function WalletPanel() {
|
|
|
360
609
|
>
|
|
361
610
|
{reassigning ? 'Cancel' : 'Reassign'}
|
|
362
611
|
</button>
|
|
612
|
+
{selectedWallet.id !== selectedAgentActiveWalletId && !selectedWallet.isActive && (
|
|
613
|
+
<button
|
|
614
|
+
type="button"
|
|
615
|
+
onClick={setDefaultWallet}
|
|
616
|
+
disabled={settingDefault}
|
|
617
|
+
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"
|
|
618
|
+
style={{ fontFamily: 'inherit' }}
|
|
619
|
+
>
|
|
620
|
+
{settingDefault ? 'Setting...' : 'Set Default'}
|
|
621
|
+
</button>
|
|
622
|
+
)}
|
|
363
623
|
</div>
|
|
364
624
|
{reassigning && (
|
|
365
625
|
<div className="mb-2 space-y-2" style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}>
|
|
366
626
|
<p className="text-[11px] text-text-3/60">Select a new agent to control this wallet:</p>
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
627
|
+
{reassignCandidates.length > 0 ? (
|
|
628
|
+
<AgentPickerList
|
|
629
|
+
agents={reassignCandidates}
|
|
630
|
+
selected=""
|
|
631
|
+
onSelect={async (agentId) => {
|
|
632
|
+
setReassignSaving(true)
|
|
633
|
+
setReassignError('')
|
|
634
|
+
try {
|
|
635
|
+
await api('PATCH', `/wallets/${selectedWallet.id}`, { agentId })
|
|
636
|
+
setReassigning(false)
|
|
637
|
+
loadWallets()
|
|
638
|
+
} catch (err: unknown) {
|
|
639
|
+
setReassignError(err instanceof Error ? err.message : String(err) || 'Reassign failed')
|
|
640
|
+
}
|
|
641
|
+
setReassignSaving(false)
|
|
642
|
+
}}
|
|
643
|
+
maxHeight={160}
|
|
644
|
+
/>
|
|
645
|
+
) : (
|
|
646
|
+
<p className="text-[10px] text-text-3/50">
|
|
647
|
+
No other agents can take this {selectedWallet.chain} wallet right now.
|
|
648
|
+
</p>
|
|
649
|
+
)}
|
|
384
650
|
{reassignSaving && <p className="text-[10px] text-text-3/50">Reassigning...</p>}
|
|
385
651
|
{reassignError && <p className="text-[10px] text-red-400">{reassignError}</p>}
|
|
386
652
|
</div>
|
|
@@ -406,21 +672,61 @@ export function WalletPanel() {
|
|
|
406
672
|
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-2">Balance</div>
|
|
407
673
|
<div className="flex items-baseline gap-3">
|
|
408
674
|
<div className="text-[28px] font-600 text-text-1 tracking-tight">
|
|
409
|
-
{
|
|
675
|
+
{selectedWalletBalance} <span className="text-[14px] text-text-3/60 font-mono">{selectedWalletSymbol}</span>
|
|
410
676
|
</div>
|
|
411
|
-
{selectedWallet.chain
|
|
412
|
-
|
|
413
|
-
|
|
677
|
+
<ChainIcon chain={selectedWallet.chain} size={16} shimmer className="opacity-80" />
|
|
678
|
+
</div>
|
|
679
|
+
<div className="mt-2 text-[11px] text-text-3/60">
|
|
680
|
+
{selectedWallet.portfolioSummary?.nonZeroAssets
|
|
681
|
+
? `${selectedWallet.portfolioSummary.nonZeroAssets} funded asset${selectedWallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'} across ${Math.max(selectedWallet.portfolioSummary.networkCount, 1)} network${selectedWallet.portfolioSummary.networkCount === 1 ? '' : 's'}`
|
|
682
|
+
: 'No funded assets detected yet.'}
|
|
414
683
|
</div>
|
|
415
684
|
</div>
|
|
416
685
|
|
|
686
|
+
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
687
|
+
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.13s both' }}>
|
|
688
|
+
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Detected Assets</div>
|
|
689
|
+
{selectedWalletAssets.length === 0 ? (
|
|
690
|
+
<p className="text-[12px] text-text-3/55">No funded token or native balances detected yet.</p>
|
|
691
|
+
) : (
|
|
692
|
+
<div className="space-y-2">
|
|
693
|
+
{selectedWalletAssets.map((asset) => (
|
|
694
|
+
<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">
|
|
695
|
+
<div className="min-w-0">
|
|
696
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
697
|
+
<span className="text-[12px] font-600 text-text-1 truncate">{asset.symbol}</span>
|
|
698
|
+
<span className="text-[10px] text-text-3/55 uppercase tracking-wide">{asset.networkLabel}</span>
|
|
699
|
+
{asset.isNative && (
|
|
700
|
+
<span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/30 text-accent-bright text-[8px] font-700 uppercase tracking-wide">
|
|
701
|
+
Gas
|
|
702
|
+
</span>
|
|
703
|
+
)}
|
|
704
|
+
</div>
|
|
705
|
+
<div className="text-[10px] text-text-3/55 truncate">{asset.name || asset.symbol}</div>
|
|
706
|
+
</div>
|
|
707
|
+
<div className="text-right shrink-0">
|
|
708
|
+
<div className="text-[12px] font-600 text-text-1">{asset.balanceDisplay || asset.balanceFormatted || asset.balanceAtomic}</div>
|
|
709
|
+
{asset.contractAddress && (
|
|
710
|
+
<div className="text-[10px] text-text-3/45 font-mono">{asset.contractAddress.slice(0, 6)}...{asset.contractAddress.slice(-4)}</div>
|
|
711
|
+
)}
|
|
712
|
+
{asset.tokenMint && (
|
|
713
|
+
<div className="text-[10px] text-text-3/45 font-mono">{asset.tokenMint.slice(0, 6)}...{asset.tokenMint.slice(-4)}</div>
|
|
714
|
+
)}
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
))}
|
|
718
|
+
</div>
|
|
719
|
+
)}
|
|
720
|
+
</div>
|
|
721
|
+
|
|
417
722
|
{/* Funding help */}
|
|
418
723
|
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
|
|
419
|
-
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.
|
|
724
|
+
style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.16s both' }}>
|
|
420
725
|
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-2">How to Fund This Wallet</div>
|
|
421
726
|
<div className="space-y-2 text-[12px] text-text-3/70 leading-relaxed">
|
|
422
|
-
|
|
423
|
-
|
|
727
|
+
{selectedWalletMeta?.fundingInstructions.map((line) => (
|
|
728
|
+
<p key={line}>{line}</p>
|
|
729
|
+
))}
|
|
424
730
|
<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>
|
|
425
731
|
</div>
|
|
426
732
|
</div>
|
|
@@ -432,13 +738,15 @@ export function WalletPanel() {
|
|
|
432
738
|
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Balance Over Time</div>
|
|
433
739
|
<div className="h-[120px] flex items-end gap-[2px]">
|
|
434
740
|
{(() => {
|
|
435
|
-
const
|
|
436
|
-
|
|
741
|
+
const recentHistory = balanceHistory.slice(-60)
|
|
742
|
+
const balances = recentHistory.map((snapshot) => Number.parseFloat(formatWalletAmount(selectedWallet.chain, getWalletBalanceAtomic(snapshot), { maxFractionDigits: 6 })) || 0)
|
|
743
|
+
const max = Math.max(...balances, 1)
|
|
744
|
+
return recentHistory.map((s, i) => (
|
|
437
745
|
<div
|
|
438
746
|
key={s.id || i}
|
|
439
747
|
className="flex-1 bg-accent/40 rounded-t-[2px] min-w-[3px] transition-all hover:bg-accent hover:scale-y-110"
|
|
440
|
-
style={{ height: `${Math.max(2, (
|
|
441
|
-
title={`${(
|
|
748
|
+
style={{ height: `${Math.max(2, ((balances[i] || 0) / max) * 100)}%`, transitionDelay: `${i * 10}ms` }}
|
|
749
|
+
title={`${formatWalletAmount(selectedWallet.chain, getWalletBalanceAtomic(s), { minFractionDigits: 4, maxFractionDigits: 6 })} ${selectedWalletSymbol} — ${new Date(s.timestamp).toLocaleString()}`}
|
|
442
750
|
/>
|
|
443
751
|
))
|
|
444
752
|
})()}
|
|
@@ -466,7 +774,7 @@ export function WalletPanel() {
|
|
|
466
774
|
{editingLimits ? (
|
|
467
775
|
<div className="space-y-3" style={{ animation: 'fade-in 0.3s ease' }}>
|
|
468
776
|
<div>
|
|
469
|
-
<label className="block text-[11px] text-text-3/70 mb-1">Per-transaction limit (
|
|
777
|
+
<label className="block text-[11px] text-text-3/70 mb-1">Per-transaction limit ({selectedWalletSymbol})</label>
|
|
470
778
|
<input
|
|
471
779
|
type="number"
|
|
472
780
|
step="0.01"
|
|
@@ -477,7 +785,7 @@ export function WalletPanel() {
|
|
|
477
785
|
/>
|
|
478
786
|
</div>
|
|
479
787
|
<div>
|
|
480
|
-
<label className="block text-[11px] text-text-3/70 mb-1">Daily limit (
|
|
788
|
+
<label className="block text-[11px] text-text-3/70 mb-1">Daily limit ({selectedWalletSymbol})</label>
|
|
481
789
|
<input
|
|
482
790
|
type="number"
|
|
483
791
|
step="0.1"
|
|
@@ -521,11 +829,11 @@ export function WalletPanel() {
|
|
|
521
829
|
<div className="space-y-2 text-[12px]">
|
|
522
830
|
<div className="flex justify-between">
|
|
523
831
|
<span className="text-text-3/70">Per-transaction</span>
|
|
524
|
-
<span className="text-text-2">{(
|
|
832
|
+
<span className="text-text-2">{formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'perTx'), { maxFractionDigits: 6 })} {selectedWalletSymbol}</span>
|
|
525
833
|
</div>
|
|
526
834
|
<div className="flex justify-between">
|
|
527
835
|
<span className="text-text-3/70">Daily rolling</span>
|
|
528
|
-
<span className="text-text-2">{(
|
|
836
|
+
<span className="text-text-2">{formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'daily'), { maxFractionDigits: 6 })} {selectedWalletSymbol}</span>
|
|
529
837
|
</div>
|
|
530
838
|
<div className="flex justify-between">
|
|
531
839
|
<span className="text-text-3/70">Approval</span>
|
|
@@ -537,12 +845,45 @@ export function WalletPanel() {
|
|
|
537
845
|
|
|
538
846
|
{/* Transaction history */}
|
|
539
847
|
<div style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.3s both' }}>
|
|
540
|
-
<div className="
|
|
541
|
-
|
|
848
|
+
<div className="flex items-center justify-between gap-3 mb-3">
|
|
849
|
+
<div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600">Transactions</div>
|
|
850
|
+
<div className="text-[10px] text-text-3/45">
|
|
851
|
+
{filteredTransactions.length}{filteredTransactions.length !== transactions.length ? ` / ${transactions.length}` : ''} shown
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center mb-3">
|
|
855
|
+
<input
|
|
856
|
+
type="text"
|
|
857
|
+
value={transactionQuery}
|
|
858
|
+
onChange={(e) => setTransactionQuery(e.target.value)}
|
|
859
|
+
placeholder="Search memo, hash, address..."
|
|
860
|
+
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"
|
|
861
|
+
style={{ fontFamily: 'inherit' }}
|
|
862
|
+
/>
|
|
863
|
+
<select
|
|
864
|
+
value={transactionFilter}
|
|
865
|
+
onChange={(e) => setTransactionFilter(e.target.value as WalletTransactionFilter)}
|
|
866
|
+
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"
|
|
867
|
+
style={{ fontFamily: 'inherit' }}
|
|
868
|
+
>
|
|
869
|
+
<option value="all">All</option>
|
|
870
|
+
<option value="confirmed">Confirmed</option>
|
|
871
|
+
<option value="pending">Pending</option>
|
|
872
|
+
<option value="failed">Failed</option>
|
|
873
|
+
<option value="send">Sends</option>
|
|
874
|
+
<option value="receive">Receives</option>
|
|
875
|
+
<option value="swap">Swaps</option>
|
|
876
|
+
</select>
|
|
877
|
+
</div>
|
|
878
|
+
{transactionsLoading && transactions.length === 0 ? (
|
|
879
|
+
<p className="text-[12px] text-text-3/50">Loading transactions...</p>
|
|
880
|
+
) : transactions.length === 0 ? (
|
|
542
881
|
<p className="text-[12px] text-text-3/50">No transactions yet.</p>
|
|
882
|
+
) : filteredTransactions.length === 0 ? (
|
|
883
|
+
<p className="text-[12px] text-text-3/50">No matching transactions.</p>
|
|
543
884
|
) : (
|
|
544
|
-
<div className="space-y-2">
|
|
545
|
-
{
|
|
885
|
+
<div className="max-h-[420px] overflow-y-auto pr-1 space-y-2">
|
|
886
|
+
{filteredTransactions.map((tx, idx) => (
|
|
546
887
|
<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"
|
|
547
888
|
style={{ animation: 'fade-up 0.4s var(--ease-spring) both', animationDelay: `${0.35 + idx * 0.03}s` }}>
|
|
548
889
|
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-[12px] ${
|
|
@@ -555,22 +896,26 @@ export function WalletPanel() {
|
|
|
555
896
|
<div className="flex-1 min-w-0">
|
|
556
897
|
<div className="flex items-center gap-2">
|
|
557
898
|
<span className="text-[12px] font-600 text-text-1">
|
|
558
|
-
{tx.type === 'send' ? '-' : '+'}{(tx.
|
|
899
|
+
{tx.type === 'send' ? '-' : '+'}{formatWalletAmount(tx.chain, getWalletAtomicAmount(tx), { minFractionDigits: 4, maxFractionDigits: 6 })} {getWalletAssetSymbol(tx.chain)}
|
|
559
900
|
</span>
|
|
560
901
|
<span className={`px-1.5 py-0.5 rounded-[4px] text-[9px] font-600 uppercase ${
|
|
561
|
-
tx.status === 'confirmed' ? 'bg-green-500/15 text-green-400' :
|
|
562
|
-
tx.status === '
|
|
563
|
-
tx.status === 'failed' ? 'bg-red-500/15 text-red-400' :
|
|
564
|
-
tx.status === 'denied' ? 'bg-red-500/15 text-red-400' :
|
|
902
|
+
getWalletTransactionStatusGroup(tx.status) === 'confirmed' ? 'bg-green-500/15 text-green-400' :
|
|
903
|
+
getWalletTransactionStatusGroup(tx.status) === 'pending' ? 'bg-amber-500/15 text-amber-400 animate-pulse' :
|
|
565
904
|
'bg-blue-500/15 text-blue-400'
|
|
566
905
|
}`}>
|
|
567
906
|
{tx.status.replace('_', ' ')}
|
|
568
907
|
</span>
|
|
908
|
+
<span className="px-1.5 py-0.5 rounded-[4px] bg-white/[0.05] text-[9px] font-600 uppercase text-text-3/70">
|
|
909
|
+
{tx.type}
|
|
910
|
+
</span>
|
|
569
911
|
</div>
|
|
570
912
|
<div className="text-[10px] text-text-3/50 font-mono truncate mt-0.5">
|
|
571
913
|
{tx.type === 'send' ? `To: ${tx.toAddress.slice(0, 8)}...${tx.toAddress.slice(-4)}` : `From: ${tx.fromAddress.slice(0, 8)}...${tx.fromAddress.slice(-4)}`}
|
|
572
914
|
</div>
|
|
573
915
|
{tx.memo && <div className="text-[10px] text-text-3/60 mt-0.5 truncate">{tx.memo}</div>}
|
|
916
|
+
<div className="text-[10px] text-text-3/40 font-mono truncate mt-0.5">
|
|
917
|
+
{tx.signature.slice(0, 10)}...{tx.signature.slice(-6)}
|
|
918
|
+
</div>
|
|
574
919
|
</div>
|
|
575
920
|
<div className="text-[10px] text-text-3/40 shrink-0">
|
|
576
921
|
{new Date(tx.timestamp).toLocaleDateString()}
|