@swarmclawai/swarmclaw 1.2.8 → 1.3.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.
Files changed (214) hide show
  1. package/README.md +39 -6
  2. package/package.json +2 -2
  3. package/src/app/agents/[id]/page.tsx +1 -18
  4. package/src/app/api/activity/route.ts +9 -23
  5. package/src/app/api/agents/route.ts +17 -1
  6. package/src/app/api/agents/thread-route.test.ts +0 -1
  7. package/src/app/api/approvals/route.test.ts +6 -22
  8. package/src/app/api/approvals/route.ts +13 -5
  9. package/src/app/api/connectors/route.ts +2 -2
  10. package/src/app/api/credentials/[id]/route.ts +2 -0
  11. package/src/app/api/credentials/route.ts +4 -1
  12. package/src/app/api/goals/[id]/route.ts +28 -0
  13. package/src/app/api/goals/route.ts +33 -0
  14. package/src/app/api/portability/export/route.ts +8 -0
  15. package/src/app/api/portability/import/route.test.ts +80 -0
  16. package/src/app/api/portability/import/route.ts +28 -0
  17. package/src/app/api/protocols/templates/[id]/route.ts +2 -1
  18. package/src/app/api/protocols/templates/route.ts +2 -1
  19. package/src/app/api/settings/route.ts +13 -2
  20. package/src/app/api/wallets/[id]/route.ts +15 -157
  21. package/src/app/api/wallets/generate/route.ts +22 -0
  22. package/src/app/api/wallets/route.test.ts +147 -0
  23. package/src/app/api/wallets/route.ts +13 -95
  24. package/src/app/autonomy/page.tsx +2 -57
  25. package/src/app/home/page.tsx +3 -0
  26. package/src/app/protocols/page.tsx +2 -21
  27. package/src/app/settings/page.tsx +0 -9
  28. package/src/app/wallets/page.tsx +105 -5
  29. package/src/cli/index.js +32 -33
  30. package/src/cli/spec.js +26 -27
  31. package/src/components/agents/agent-sheet.tsx +2 -40
  32. package/src/components/agents/inspector-panel.tsx +0 -83
  33. package/src/components/chat/chat-card.tsx +0 -31
  34. package/src/components/chat/message-bubble.tsx +1 -108
  35. package/src/components/connectors/connector-sheet.tsx +25 -1
  36. package/src/components/layout/sidebar-rail.tsx +6 -10
  37. package/src/components/projects/project-detail.tsx +3 -35
  38. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  39. package/src/components/projects/tabs/work-tab.tsx +7 -77
  40. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  41. package/src/components/shared/connector-platform-icon.tsx +1 -0
  42. package/src/components/tasks/task-card.tsx +4 -34
  43. package/src/components/tasks/task-sheet.tsx +6 -36
  44. package/src/components/wallets/wallet-list.tsx +150 -0
  45. package/src/lib/app/navigation.test.ts +0 -13
  46. package/src/lib/app/navigation.ts +2 -7
  47. package/src/lib/app/view-constants.ts +14 -19
  48. package/src/lib/server/activity/activity-log.ts +16 -1
  49. package/src/lib/server/agents/agent-service.ts +24 -11
  50. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  51. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  52. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  53. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  54. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  55. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  56. package/src/lib/server/approval-match.ts +14 -85
  57. package/src/lib/server/approvals/approval-hooks.ts +81 -0
  58. package/src/lib/server/approvals.test.ts +6 -6
  59. package/src/lib/server/approvals.ts +11 -6
  60. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  61. package/src/lib/server/builtin-extensions.ts +0 -2
  62. package/src/lib/server/capability-router.test.ts +0 -2
  63. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
  64. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  65. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
  66. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  67. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  68. package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
  69. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  70. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  71. package/src/lib/server/chat-execution/message-classifier.ts +1 -16
  72. package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
  73. package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
  74. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  75. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  76. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  77. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
  78. package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
  79. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  80. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  81. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  82. package/src/lib/server/chats/chat-session-service.ts +3 -5
  83. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  84. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  85. package/src/lib/server/connectors/connector-service.ts +39 -9
  86. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  87. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  88. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  89. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  90. package/src/lib/server/connectors/swarmdock-tasks.ts +127 -0
  91. package/src/lib/server/connectors/swarmdock.ts +285 -0
  92. package/src/lib/server/execution-brief.test.ts +2 -25
  93. package/src/lib/server/execution-brief.ts +30 -35
  94. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  95. package/src/lib/server/goals/goal-repository.ts +19 -0
  96. package/src/lib/server/goals/goal-service.ts +143 -0
  97. package/src/lib/server/persistence/storage-context.ts +0 -5
  98. package/src/lib/server/portability/export.ts +109 -0
  99. package/src/lib/server/portability/import.ts +159 -0
  100. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  101. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  102. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  103. package/src/lib/server/protocols/protocol-service.ts +0 -1
  104. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  105. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  106. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  107. package/src/lib/server/protocols/protocol-types.ts +0 -2
  108. package/src/lib/server/provider-health.ts +0 -9
  109. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  110. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  111. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  112. package/src/lib/server/runtime/queue/core.ts +11 -33
  113. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  114. package/src/lib/server/runtime/scheduler.ts +0 -13
  115. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  116. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  117. package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
  118. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  119. package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
  120. package/src/lib/server/session-tools/crud.ts +0 -14
  121. package/src/lib/server/session-tools/delegate.ts +0 -4
  122. package/src/lib/server/session-tools/index.ts +0 -4
  123. package/src/lib/server/session-tools/team-context.ts +0 -3
  124. package/src/lib/server/storage-normalization.ts +13 -0
  125. package/src/lib/server/storage.ts +75 -45
  126. package/src/lib/server/tasks/task-checkout.ts +59 -0
  127. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  128. package/src/lib/server/tasks/task-route-service.ts +4 -26
  129. package/src/lib/server/tasks/task-service.ts +0 -7
  130. package/src/lib/server/tool-aliases.ts +0 -1
  131. package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
  132. package/src/lib/server/tool-capability-policy.ts +0 -2
  133. package/src/lib/server/tool-planning.ts +0 -12
  134. package/src/lib/server/universal-tool-access.ts +0 -1
  135. package/src/lib/server/usage/cost-rollup.ts +124 -0
  136. package/src/lib/server/usage/usage-repository.ts +6 -0
  137. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  138. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  139. package/src/lib/server/wallets/wallet-service.ts +119 -0
  140. package/src/lib/server/working-state/extraction.ts +8 -42
  141. package/src/lib/server/working-state/normalization.ts +10 -103
  142. package/src/lib/server/working-state/service.ts +12 -21
  143. package/src/lib/strip-internal-metadata.test.ts +1 -1
  144. package/src/lib/strip-internal-metadata.ts +1 -1
  145. package/src/lib/tool-definitions.ts +0 -1
  146. package/src/lib/validation/schemas.ts +36 -32
  147. package/src/lib/validation/server-schemas.ts +35 -0
  148. package/src/stores/slices/data-slice.ts +5 -1
  149. package/src/stores/slices/ui-slice.ts +0 -4
  150. package/src/types/agent.ts +10 -84
  151. package/src/types/app-settings.ts +6 -2
  152. package/src/types/approval.ts +3 -2
  153. package/src/types/connector.ts +1 -0
  154. package/src/types/goal.ts +30 -0
  155. package/src/types/index.ts +2 -1
  156. package/src/types/message.ts +0 -1
  157. package/src/types/misc.ts +2 -4
  158. package/src/types/protocol.ts +0 -2
  159. package/src/types/run.ts +0 -3
  160. package/src/types/session.ts +1 -51
  161. package/src/types/swarmdock.ts +29 -0
  162. package/src/types/task.ts +9 -3
  163. package/src/types/working-state.ts +2 -9
  164. package/src/views/settings/section-runtime-loop.tsx +0 -14
  165. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  166. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  167. package/src/app/api/missions/[id]/events/route.ts +0 -14
  168. package/src/app/api/missions/[id]/route.ts +0 -10
  169. package/src/app/api/missions/route.test.ts +0 -244
  170. package/src/app/api/missions/route.ts +0 -57
  171. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  172. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  173. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  174. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  175. package/src/app/missions/[id]/page.tsx +0 -3
  176. package/src/app/missions/page.tsx +0 -685
  177. package/src/components/canvas/canvas-panel.tsx +0 -267
  178. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  179. package/src/components/wallets/wallet-panel.tsx +0 -1010
  180. package/src/components/wallets/wallet-section.tsx +0 -260
  181. package/src/features/missions/queries.ts +0 -23
  182. package/src/lib/canvas-content.test.ts +0 -360
  183. package/src/lib/canvas-content.ts +0 -198
  184. package/src/lib/server/canvas-content.test.ts +0 -32
  185. package/src/lib/server/canvas-content.ts +0 -6
  186. package/src/lib/server/ethereum.ts +0 -591
  187. package/src/lib/server/evm-swap.ts +0 -476
  188. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  189. package/src/lib/server/missions/mission-intent.ts +0 -569
  190. package/src/lib/server/missions/mission-repository.ts +0 -74
  191. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  192. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  193. package/src/lib/server/missions/mission-service/context.ts +0 -4
  194. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  195. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  196. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  197. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  198. package/src/lib/server/missions/mission-service.test.ts +0 -888
  199. package/src/lib/server/missions/mission-service.ts +0 -6
  200. package/src/lib/server/session-tools/canvas.ts +0 -105
  201. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  202. package/src/lib/server/session-tools/wallet.ts +0 -1287
  203. package/src/lib/server/solana.ts +0 -327
  204. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  205. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  206. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  207. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  208. package/src/lib/server/wallet/wallet-service.ts +0 -225
  209. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  210. package/src/lib/wallet/wallet-transactions.ts +0 -43
  211. package/src/lib/wallet/wallet.test.ts +0 -333
  212. package/src/lib/wallet/wallet.ts +0 -183
  213. package/src/types/mission.ts +0 -185
  214. 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
- }