@swarmclawai/swarmclaw 0.7.8 → 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.
Files changed (251) hide show
  1. package/README.md +12 -15
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -1,12 +1,46 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadWallets, upsertWallet, deleteWallet as deleteWalletFromStore, loadAgents, saveAgents } from '@/lib/server/storage'
3
- import { getBalance, lamportsToSol } from '@/lib/server/solana'
4
3
  import { notify } from '@/lib/server/ws-hub'
5
- import type { AgentWallet } from '@/types'
4
+ import { getWalletLimitAtomic, normalizeAtomicString } from '@/lib/wallet'
5
+ import type { AgentWallet, WalletAssetBalance, WalletPortfolioSummary } from '@/types'
6
+ import { buildEmptyWalletPortfolio } from '@/lib/server/wallet-portfolio'
7
+ import {
8
+ getAgentActiveWalletId,
9
+ getWalletPortfolioSnapshot,
10
+ linkWalletToAgent,
11
+ setAgentActiveWallet,
12
+ stripWalletPrivateKey,
13
+ unlinkWalletFromAgent,
14
+ } from '@/lib/server/wallet-service'
6
15
  export const dynamic = 'force-dynamic'
7
-
8
- function stripPrivateKey(wallet: Record<string, unknown>): Record<string, unknown> {
9
- return Object.fromEntries(Object.entries(wallet).filter(([k]) => k !== 'encryptedPrivateKey'))
16
+ const WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS = 2500
17
+
18
+ function withPortfolio(
19
+ wallet: AgentWallet,
20
+ portfolio: {
21
+ balanceAtomic: string
22
+ balanceFormatted: string
23
+ balanceSymbol: string
24
+ balanceDisplay: string
25
+ balanceLamports?: number
26
+ balanceSol?: number
27
+ assets: WalletAssetBalance[]
28
+ summary: WalletPortfolioSummary
29
+ },
30
+ isActive: boolean,
31
+ ) {
32
+ return {
33
+ ...stripWalletPrivateKey(wallet as unknown as Record<string, unknown>),
34
+ balanceAtomic: portfolio.balanceAtomic,
35
+ balanceFormatted: portfolio.balanceFormatted,
36
+ balanceSymbol: portfolio.balanceSymbol,
37
+ balanceDisplay: portfolio.balanceDisplay,
38
+ balanceLamports: portfolio.balanceLamports,
39
+ balanceSol: portfolio.balanceSol,
40
+ assets: portfolio.assets,
41
+ portfolioSummary: portfolio.summary,
42
+ isActive,
43
+ }
10
44
  }
11
45
 
12
46
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -15,21 +49,19 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
15
49
  const wallet = wallets[id]
16
50
  if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
17
51
 
18
- // Fetch live on-chain balance
19
- let balanceLamports = 0
20
- let balanceSol = 0
52
+ let portfolio = buildEmptyWalletPortfolio(wallet)
21
53
  try {
22
- balanceLamports = await getBalance(wallet.publicKey)
23
- balanceSol = lamportsToSol(balanceLamports)
54
+ portfolio = await getWalletPortfolioSnapshot(wallet, {
55
+ timeoutMs: WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS,
56
+ allowStale: true,
57
+ })
24
58
  } catch {
25
59
  // RPC failure — return 0
26
60
  }
27
61
 
28
- return NextResponse.json({
29
- ...stripPrivateKey(wallet as unknown as Record<string, unknown>),
30
- balanceLamports,
31
- balanceSol,
32
- })
62
+ const agents = loadAgents()
63
+ const isActive = getAgentActiveWalletId(agents[wallet.agentId]) === wallet.id
64
+ return NextResponse.json(withPortfolio(wallet, portfolio, isActive))
33
65
  }
34
66
 
35
67
  export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -39,6 +71,7 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
39
71
  if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
40
72
 
41
73
  const body = await req.json()
74
+ const shouldMakeActive = body.makeActive === true
42
75
 
43
76
  // Reassign wallet to a different agent
44
77
  if (typeof body.agentId === 'string' && body.agentId !== wallet.agentId) {
@@ -46,39 +79,51 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
46
79
  const newAgent = agents[body.agentId]
47
80
  if (!newAgent) return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
48
81
 
49
- // Check new agent doesn't already have a wallet
82
+ // Only one wallet per chain per agent.
50
83
  const allWallets = loadWallets() as Record<string, AgentWallet>
51
- const conflict = Object.values(allWallets).find((w) => w.agentId === body.agentId && w.id !== id)
52
- if (conflict) return NextResponse.json({ error: 'Target agent already has a wallet' }, { status: 409 })
84
+ const conflict = Object.values(allWallets).find((w) => w.agentId === body.agentId && w.id !== id && w.chain === wallet.chain)
85
+ if (conflict) return NextResponse.json({ error: `Target agent already has a ${wallet.chain} wallet` }, { status: 409 })
53
86
 
54
- // Unlink old agent
55
87
  const oldAgent = agents[wallet.agentId]
56
88
  if (oldAgent) {
57
- oldAgent.walletId = null
89
+ unlinkWalletFromAgent(oldAgent, id)
58
90
  oldAgent.updatedAt = Date.now()
59
91
  agents[wallet.agentId] = oldAgent
60
92
  }
61
93
 
62
- // Link new agent
63
- newAgent.walletId = id
94
+ linkWalletToAgent(newAgent, id, shouldMakeActive || getAgentActiveWalletId(newAgent) == null)
64
95
  newAgent.updatedAt = Date.now()
65
96
  agents[body.agentId] = newAgent
66
97
  saveAgents(agents)
67
98
  notify('agents')
68
99
 
69
100
  wallet.agentId = body.agentId
101
+ } else if (shouldMakeActive) {
102
+ const agents = loadAgents()
103
+ const agent = agents[wallet.agentId]
104
+ if (agent) {
105
+ setAgentActiveWallet(agent, id)
106
+ agent.updatedAt = Date.now()
107
+ agents[wallet.agentId] = agent
108
+ saveAgents(agents)
109
+ notify('agents')
110
+ }
70
111
  }
71
112
 
72
113
  if (body.label !== undefined) wallet.label = body.label
73
- if (typeof body.spendingLimitLamports === 'number') wallet.spendingLimitLamports = body.spendingLimitLamports
74
- if (typeof body.dailyLimitLamports === 'number') wallet.dailyLimitLamports = body.dailyLimitLamports
114
+ if (body.spendingLimitAtomic !== undefined || body.spendingLimitLamports !== undefined) {
115
+ wallet.spendingLimitAtomic = normalizeAtomicString(body.spendingLimitAtomic ?? body.spendingLimitLamports, getWalletLimitAtomic(wallet, 'perTx'))
116
+ }
117
+ if (body.dailyLimitAtomic !== undefined || body.dailyLimitLamports !== undefined) {
118
+ wallet.dailyLimitAtomic = normalizeAtomicString(body.dailyLimitAtomic ?? body.dailyLimitLamports, getWalletLimitAtomic(wallet, 'daily'))
119
+ }
75
120
  if (typeof body.requireApproval === 'boolean') wallet.requireApproval = body.requireApproval
76
121
  wallet.updatedAt = Date.now()
77
122
 
78
123
  upsertWallet(id, wallet)
79
124
  notify('wallets')
80
125
 
81
- return NextResponse.json(stripPrivateKey(wallet as unknown as Record<string, unknown>))
126
+ return NextResponse.json(stripWalletPrivateKey(wallet as unknown as Record<string, unknown>))
82
127
  }
83
128
 
84
129
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -88,20 +133,19 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
88
133
  if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
89
134
 
90
135
  // Check if balance > 0 and warn
91
- let balanceLamports = 0
136
+ let portfolio = buildEmptyWalletPortfolio(wallet)
92
137
  try {
93
- balanceLamports = await getBalance(wallet.publicKey)
138
+ portfolio = await getWalletPortfolioSnapshot(wallet, {
139
+ timeoutMs: WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS,
140
+ allowStale: true,
141
+ })
94
142
  } catch { /* ignore */ }
95
143
 
96
- if (balanceLamports > 0) {
97
- // Still delete, but include warning
98
- }
99
-
100
144
  // Unlink from agent
101
145
  const agents = loadAgents()
102
146
  const agent = agents[wallet.agentId]
103
147
  if (agent) {
104
- agent.walletId = null
148
+ unlinkWalletFromAgent(agent, id)
105
149
  agent.updatedAt = Date.now()
106
150
  agents[wallet.agentId] = agent
107
151
  saveAgents(agents)
@@ -113,6 +157,8 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
113
157
 
114
158
  return NextResponse.json({
115
159
  ok: true,
116
- warning: balanceLamports > 0 ? `Wallet had ${lamportsToSol(balanceLamports)} SOL remaining` : undefined,
160
+ warning: portfolio.summary.nonZeroAssets > 0
161
+ ? `Wallet still had ${portfolio.summary.nonZeroAssets} asset${portfolio.summary.nonZeroAssets === 1 ? '' : 's'} remaining, including ${portfolio.balanceDisplay}`
162
+ : undefined,
117
163
  })
118
164
  }
@@ -1,9 +1,12 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
- import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
4
- import { sendSol, isValidSolanaAddress, lamportsToSol } from '@/lib/server/solana'
3
+ import { loadWallets, upsertWalletTransaction } from '@/lib/server/storage'
5
4
  import { notify } from '@/lib/server/ws-hub'
6
5
  import type { AgentWallet, WalletTransaction } from '@/types'
6
+ import {
7
+ normalizeAtomicString,
8
+ } from '@/lib/wallet'
9
+ import { isValidWalletAddress, sendWalletNativeAsset, validateWalletSendLimits } from '@/lib/server/wallet-service'
7
10
  export const dynamic = 'force-dynamic'
8
11
 
9
12
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -14,36 +17,15 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
14
17
 
15
18
  const body = await req.json()
16
19
  const toAddress = typeof body.toAddress === 'string' ? body.toAddress.trim() : ''
17
- const amountLamports = typeof body.amountLamports === 'number' ? Math.floor(body.amountLamports) : 0
20
+ const amountAtomic = normalizeAtomicString(body.amountAtomic ?? body.amountLamports, '0')
18
21
  const memo = typeof body.memo === 'string' ? body.memo.slice(0, 500) : undefined
19
22
 
20
- if (!toAddress || !isValidSolanaAddress(toAddress)) {
23
+ if (!toAddress || !isValidWalletAddress(wallet.chain, toAddress)) {
21
24
  return NextResponse.json({ error: 'Invalid recipient address' }, { status: 400 })
22
25
  }
23
- if (amountLamports <= 0) {
24
- return NextResponse.json({ error: 'Amount must be positive' }, { status: 400 })
25
- }
26
-
27
- // Per-tx spending limit
28
- const perTxLimit = wallet.spendingLimitLamports ?? 100_000_000
29
- if (amountLamports > perTxLimit) {
30
- return NextResponse.json({
31
- error: `Amount ${lamportsToSol(amountLamports)} SOL exceeds per-transaction limit of ${lamportsToSol(perTxLimit)} SOL`,
32
- }, { status: 403 })
33
- }
34
-
35
- // 24h rolling daily limit
36
- const dailyLimit = wallet.dailyLimitLamports ?? 1_000_000_000
37
- const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
38
- const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
39
- const recentSends = Object.values(allTxs).filter(
40
- (tx) => tx.walletId === id && tx.type === 'send' && tx.status === 'confirmed' && tx.timestamp > oneDayAgo,
41
- )
42
- const dailySpent = recentSends.reduce((sum, tx) => sum + tx.amountLamports, 0)
43
- if (dailySpent + amountLamports > dailyLimit) {
44
- return NextResponse.json({
45
- error: `Daily limit exceeded. Spent ${lamportsToSol(dailySpent)} SOL in last 24h, limit is ${lamportsToSol(dailyLimit)} SOL`,
46
- }, { status: 403 })
26
+ const limitError = validateWalletSendLimits({ wallet, amountAtomic })
27
+ if (limitError) {
28
+ return NextResponse.json({ error: limitError }, { status: limitError === 'Amount must be positive' ? 400 : 403 })
47
29
  }
48
30
 
49
31
  const txId = genId(8)
@@ -60,7 +42,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
60
42
  signature: '',
61
43
  fromAddress: wallet.publicKey,
62
44
  toAddress,
63
- amountLamports,
45
+ amountAtomic,
46
+ amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
64
47
  status: 'pending_approval',
65
48
  memo,
66
49
  timestamp: now,
@@ -72,7 +55,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
72
55
 
73
56
  // Auto-approved — sign and submit
74
57
  try {
75
- const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, toAddress, amountLamports)
58
+ const { signature, feeAtomic } = await sendWalletNativeAsset(wallet, toAddress, amountAtomic)
76
59
  const confirmedTx: WalletTransaction = {
77
60
  id: txId,
78
61
  walletId: id,
@@ -82,8 +65,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
82
65
  signature,
83
66
  fromAddress: wallet.publicKey,
84
67
  toAddress,
85
- amountLamports,
86
- feeLamports: fee,
68
+ amountAtomic,
69
+ amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
70
+ feeAtomic,
71
+ feeLamports: wallet.chain === 'solana' && feeAtomic ? Number.parseInt(feeAtomic, 10) : undefined,
87
72
  status: 'confirmed',
88
73
  memo,
89
74
  approvedBy: 'auto',
@@ -102,7 +87,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
102
87
  signature: '',
103
88
  fromAddress: wallet.publicKey,
104
89
  toAddress,
105
- amountLamports,
90
+ amountAtomic,
91
+ amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
106
92
  status: 'failed',
107
93
  memo,
108
94
  timestamp: now,
@@ -1,74 +1,91 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { genId } from '@/lib/id'
3
- import { loadWallets, upsertWallet, loadAgents, saveAgents } from '@/lib/server/storage'
4
- import { generateSolanaKeypair } from '@/lib/server/solana'
5
- import { notify } from '@/lib/server/ws-hub'
6
- import type { AgentWallet, WalletChain } from '@/types'
2
+ import { loadAgents, loadWallets } from '@/lib/server/storage'
3
+ import { createAgentWallet, getAgentActiveWalletId, getWalletPortfolioSnapshot, stripWalletPrivateKey } from '@/lib/server/wallet-service'
4
+ import { buildEmptyWalletPortfolio } from '@/lib/server/wallet-portfolio'
5
+ import type { AgentWallet, WalletPortfolioSummary } from '@/types'
7
6
  export const dynamic = 'force-dynamic'
7
+ const WALLET_LIST_PORTFOLIO_TIMEOUT_MS = 1500
8
8
 
9
- /** Strip encryptedPrivateKey from wallet for safe API responses */
10
- function stripPrivateKey(wallet: Record<string, unknown>): Record<string, unknown> {
11
- return Object.fromEntries(Object.entries(wallet).filter(([k]) => k !== 'encryptedPrivateKey'))
9
+ function withPortfolio(
10
+ wallet: AgentWallet,
11
+ portfolio: {
12
+ balanceAtomic: string
13
+ balanceFormatted: string
14
+ balanceSymbol: string
15
+ balanceDisplay: string
16
+ balanceLamports?: number
17
+ balanceSol?: number
18
+ assets: unknown[]
19
+ summary: WalletPortfolioSummary
20
+ },
21
+ isActive: boolean,
22
+ ) {
23
+ return {
24
+ ...stripWalletPrivateKey(wallet as unknown as Record<string, unknown>),
25
+ balanceAtomic: portfolio.balanceAtomic,
26
+ balanceFormatted: portfolio.balanceFormatted,
27
+ balanceSymbol: portfolio.balanceSymbol,
28
+ balanceDisplay: portfolio.balanceDisplay,
29
+ balanceLamports: portfolio.balanceLamports,
30
+ balanceSol: portfolio.balanceSol,
31
+ assets: portfolio.assets,
32
+ portfolioSummary: portfolio.summary,
33
+ isActive,
34
+ }
12
35
  }
13
36
 
14
- export async function GET() {
37
+ export async function GET(req: Request) {
15
38
  const wallets = loadWallets() as Record<string, AgentWallet>
16
- const safe = Object.fromEntries(
17
- Object.entries(wallets).map(([id, w]) => [id, stripPrivateKey(w as unknown as Record<string, unknown>)]),
39
+ const agents = loadAgents()
40
+ const { searchParams } = new URL(req.url)
41
+ const agentId = searchParams.get('agentId')?.trim() || ''
42
+ const walletEntries = Object.entries(wallets)
43
+ .filter(([, wallet]) => !agentId || wallet.agentId === agentId)
44
+ const entries = await Promise.all(
45
+ walletEntries.map(async ([id, wallet]) => {
46
+ let portfolio = buildEmptyWalletPortfolio(wallet)
47
+ try {
48
+ portfolio = await getWalletPortfolioSnapshot(wallet, {
49
+ timeoutMs: WALLET_LIST_PORTFOLIO_TIMEOUT_MS,
50
+ allowStale: true,
51
+ })
52
+ } catch {
53
+ // Slow or failed RPC discovery — return empty/stale portfolio for list view
54
+ }
55
+ const activeWalletId = getAgentActiveWalletId(agents[wallet.agentId])
56
+ return [id, withPortfolio(wallet, portfolio, activeWalletId === wallet.id)] as const
57
+ }),
18
58
  )
19
- return NextResponse.json(safe)
59
+ return NextResponse.json(Object.fromEntries(entries))
20
60
  }
21
61
 
22
62
  export async function POST(req: Request) {
23
63
  const body = await req.json()
24
- const agentId = typeof body.agentId === 'string' ? body.agentId.trim() : ''
25
- if (!agentId) {
26
- return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
27
- }
28
-
29
- const agents = loadAgents()
30
- if (!agents[agentId]) {
31
- return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
32
- }
33
-
34
- // Check agent doesn't already have a wallet
35
- const existing = loadWallets() as Record<string, AgentWallet>
36
- const hasWallet = Object.values(existing).some((w) => w.agentId === agentId)
37
- if (hasWallet) {
38
- return NextResponse.json({ error: 'Agent already has a wallet' }, { status: 409 })
64
+ try {
65
+ const wallet = createAgentWallet({
66
+ agentId: body.agentId,
67
+ chain: body.chain,
68
+ provider: body.provider,
69
+ label: body.label,
70
+ requireApproval: body.requireApproval,
71
+ spendingLimitAtomic: body.spendingLimitAtomic ?? body.spendingLimitLamports,
72
+ dailyLimitAtomic: body.dailyLimitAtomic ?? body.dailyLimitLamports,
73
+ })
74
+ return NextResponse.json(stripWalletPrivateKey(wallet as unknown as Record<string, unknown>))
75
+ } catch (err: unknown) {
76
+ const message = err instanceof Error ? err.message : String(err)
77
+ if (message === 'agentId is required') {
78
+ return NextResponse.json({ error: message }, { status: 400 })
79
+ }
80
+ if (/^Unsupported wallet chain or provider: /.test(message)) {
81
+ return NextResponse.json({ error: message }, { status: 400 })
82
+ }
83
+ if (message === 'Agent not found') {
84
+ return NextResponse.json({ error: message }, { status: 404 })
85
+ }
86
+ if (/^Agent already has a (solana|ethereum) wallet$/.test(message)) {
87
+ return NextResponse.json({ error: message }, { status: 409 })
88
+ }
89
+ return NextResponse.json({ error: message }, { status: 500 })
39
90
  }
40
-
41
- const chain: WalletChain = body.chain === 'solana' ? 'solana' : 'solana' // extensible later
42
- const { publicKey, encryptedPrivateKey } = generateSolanaKeypair()
43
-
44
- const id = genId()
45
- const now = Date.now()
46
-
47
- const wallet: AgentWallet = {
48
- id,
49
- agentId,
50
- chain,
51
- publicKey,
52
- encryptedPrivateKey,
53
- label: typeof body.label === 'string' ? body.label : undefined,
54
- spendingLimitLamports: typeof body.spendingLimitLamports === 'number' ? body.spendingLimitLamports : 100_000_000,
55
- dailyLimitLamports: typeof body.dailyLimitLamports === 'number' ? body.dailyLimitLamports : 1_000_000_000,
56
- requireApproval: body.requireApproval !== false,
57
- createdAt: now,
58
- updatedAt: now,
59
- }
60
-
61
- upsertWallet(id, wallet)
62
-
63
- // Link wallet to agent
64
- const agent = agents[agentId]
65
- agent.walletId = id
66
- agent.updatedAt = now
67
- agents[agentId] = agent
68
- saveAgents(agents)
69
-
70
- notify('wallets')
71
- notify('agents')
72
-
73
- return NextResponse.json(stripPrivateKey(wallet as unknown as Record<string, unknown>))
74
91
  }
@@ -13,6 +13,18 @@ import { triggerWebhookWatchJobs } from '@/lib/server/watch-jobs'
13
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
14
  const ops: CollectionOps<any> = { load: loadWebhooks, save: saveWebhooks }
15
15
 
16
+ type WebhookPostDeps = {
17
+ enqueueRun: typeof enqueueSessionRun
18
+ enqueueEvent: typeof enqueueSystemEvent
19
+ requestHeartbeat: typeof requestHeartbeatNow
20
+ }
21
+
22
+ const defaultWebhookPostDeps: WebhookPostDeps = {
23
+ enqueueRun: enqueueSessionRun,
24
+ enqueueEvent: enqueueSystemEvent,
25
+ requestHeartbeat: requestHeartbeatNow,
26
+ }
27
+
16
28
  function normalizeEvents(value: unknown): string[] {
17
29
  if (!Array.isArray(value)) return []
18
30
  return value
@@ -58,8 +70,11 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
58
70
  return NextResponse.json({ ok: true })
59
71
  }
60
72
 
61
- export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
62
- const { id } = await params
73
+ export async function handleWebhookPost(
74
+ req: Request,
75
+ id: string,
76
+ deps: WebhookPostDeps = defaultWebhookPostDeps,
77
+ ) {
63
78
  const webhooks = loadWebhooks()
64
79
  const webhook = webhooks[id]
65
80
  if (!webhook) return notFound('Webhook not found')
@@ -176,7 +191,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
176
191
  agentId: agent.id,
177
192
  parentSessionId: null,
178
193
  tools: agent.tools || [],
179
- heartbeatEnabled: agent.heartbeatEnabled ?? true,
194
+ heartbeatEnabled: agent.heartbeatEnabled ?? false,
180
195
  heartbeatIntervalSec: agent.heartbeatIntervalSec ?? null,
181
196
  }
182
197
  sessions[session.id as string] = session
@@ -200,7 +215,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
200
215
  ].join('\n')
201
216
 
202
217
  try {
203
- const run = enqueueSessionRun({
218
+ const run = deps.enqueueRun({
204
219
  sessionId: sid,
205
220
  message: prompt,
206
221
  source: 'webhook',
@@ -209,9 +224,16 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
209
224
  })
210
225
 
211
226
  // Enqueue system event + heartbeat wake
212
- enqueueSystemEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
227
+ deps.enqueueEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
213
228
  if (webhook.agentId) {
214
- requestHeartbeatNow({ agentId: webhook.agentId, reason: 'webhook' })
229
+ deps.requestHeartbeat({
230
+ agentId: webhook.agentId,
231
+ eventId: `webhook:${id}:${incomingEvent}:${Date.now()}`,
232
+ reason: 'webhook',
233
+ source: `webhook:${id}`,
234
+ resumeMessage: `Webhook received: ${webhook.name || id} (${incomingEvent})`,
235
+ detail: payloadPreview || '(empty payload)',
236
+ })
215
237
  }
216
238
 
217
239
  appendWebhookLog(genId(8), {
@@ -262,3 +284,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
262
284
  })
263
285
  }
264
286
  }
287
+
288
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
289
+ const { id } = await params
290
+ return handleWebhookPost(req, id)
291
+ }