@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  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 +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -1,170 +1,1275 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import crypto from 'node:crypto'
4
+
5
+ import type { ApprovalCategory, ApprovalRequest, Plugin, PluginHooks, WalletTransaction } from '@/types'
6
+ import { genId } from '@/lib/id'
7
+ import {
8
+ formatWalletAmount,
9
+ getWalletAssetSymbol,
10
+ getWalletAtomicAmount,
11
+ getWalletChainOrDefault,
12
+ getWalletExplorerUrl,
13
+ getWalletLimitAtomic,
14
+ parseDisplayAmountToAtomic,
15
+ } from '@/lib/wallet'
16
+
3
17
  import type { ToolBuildContext } from './context'
4
- import { loadWallets, loadWalletTransactions } from '../storage'
5
- import type { AgentWallet, WalletTransaction, Plugin, PluginHooks } from '@/types'
6
- import { getPluginManager } from '../plugins'
7
18
  import { normalizeToolInputArgs } from './normalize-tool-args'
19
+ import type { SolanaCluster } from '../solana'
20
+ import { buildApprovalComparablePayload } from '../approval-match'
21
+ import { isLikelyRetryableSwapError, prepareEvmSwapPlan } from '../evm-swap'
22
+ import {
23
+ callEthereumContract,
24
+ encodeEthereumContractCall,
25
+ getEvmNetworkConfig,
26
+ sendEthereumTransaction,
27
+ signEthereumMessage,
28
+ signEthereumTransaction,
29
+ signEthereumTypedData,
30
+ simulateEthereumTransaction,
31
+ } from '../ethereum'
32
+ import { getPluginManager } from '../plugins'
33
+ import { loadAgents, loadApprovals, loadWalletTransactions, upsertWalletTransaction } from '../storage'
34
+ import {
35
+ getSolanaClusterLabel,
36
+ normalizeSolanaCluster,
37
+ sendSolanaTransaction,
38
+ signSolanaMessage,
39
+ signSolanaTransaction,
40
+ simulateSolanaTransaction,
41
+ } from '../solana'
42
+ import { TOOL_CAPABILITY } from '../tool-planning'
43
+ import { clearWalletPortfolioCache } from '../wallet-portfolio'
44
+ import {
45
+ createAgentWallet,
46
+ getAgentActiveWalletId,
47
+ getWalletByAgentId,
48
+ getWalletPortfolioSnapshot,
49
+ getWalletsByAgentId,
50
+ isValidWalletAddress,
51
+ } from '../wallet-service'
8
52
 
9
- /**
10
- * Core Wallet Execution Logic
11
- */
12
- async function executeWalletAction(args: any, context: { agentId?: string | null }) {
13
- const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
14
- const action = normalized.action as string | undefined
15
- const toAddress = (normalized.toAddress ?? normalized.to) as string | undefined
16
- const amountSol = normalized.amountSol as number | undefined
17
- const memo = normalized.memo as string | undefined
18
- const limit = normalized.limit as number | undefined
19
- const agentId = context.agentId
53
+ const WALLET_TOOL_ACTIONS = [
54
+ 'setup',
55
+ 'balance',
56
+ 'address',
57
+ 'send',
58
+ 'transactions',
59
+ 'call_contract',
60
+ 'sign_message',
61
+ 'sign_typed_data',
62
+ 'encode_contract_call',
63
+ 'quote_swap',
64
+ 'simulate_transaction',
65
+ 'sign_transaction',
66
+ 'swap',
67
+ 'send_transaction',
68
+ ] as const
20
69
 
21
- if (!agentId) return JSON.stringify({ error: 'No agent ID in context' })
22
-
23
- const wallets = loadWallets() as Record<string, AgentWallet>
24
- const wallet = Object.values(wallets).find((w) => w.agentId === agentId) ?? null
70
+ type WalletToolAction = (typeof WALLET_TOOL_ACTIONS)[number]
25
71
 
26
- if (!wallet) {
27
- if (action === 'address' || action === 'balance' || action === 'transactions') {
72
+ function trimString(value: unknown): string {
73
+ return typeof value === 'string' ? value.trim() : ''
74
+ }
75
+
76
+ function parseJsonValue<T>(value: unknown, label: string): T | undefined {
77
+ if (value === undefined || value === null || value === '') return undefined
78
+ if (typeof value === 'string') {
79
+ const trimmed = value.trim()
80
+ if (!trimmed) return undefined
81
+ try {
82
+ return JSON.parse(trimmed) as T
83
+ } catch (err: unknown) {
84
+ throw new Error(`${label} must be valid JSON: ${err instanceof Error ? err.message : String(err)}`)
85
+ }
86
+ }
87
+ return value as T
88
+ }
89
+
90
+ function parseRecordValue(value: unknown, label: string): Record<string, unknown> | undefined {
91
+ const parsed = parseJsonValue<Record<string, unknown>>(value, label)
92
+ if (parsed === undefined) return undefined
93
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
94
+ throw new Error(`${label} must be an object`)
95
+ }
96
+ return parsed
97
+ }
98
+
99
+ function parseArrayValue(value: unknown, label: string): unknown[] | undefined {
100
+ const parsed = parseJsonValue<unknown[]>(value, label)
101
+ if (parsed === undefined) return undefined
102
+ if (!Array.isArray(parsed)) throw new Error(`${label} must be an array`)
103
+ return parsed
104
+ }
105
+
106
+ function parseFunctionArgsValue(value: unknown, label: string): unknown[] | Record<string, unknown> | undefined {
107
+ const parsed = parseJsonValue<unknown>(value, label)
108
+ if (parsed === undefined) return undefined
109
+ if (Array.isArray(parsed)) return parsed
110
+ if (parsed && typeof parsed === 'object') return parsed as Record<string, unknown>
111
+ throw new Error(`${label} must be a JSON array or object`)
112
+ }
113
+
114
+ function pickFirstDefined(record: Record<string, unknown>, keys: string[]): unknown {
115
+ for (const key of keys) {
116
+ if (record[key] !== undefined && record[key] !== null && record[key] !== '') return record[key]
117
+ }
118
+ return undefined
119
+ }
120
+
121
+ function describeWalletAssetIdentity(asset: {
122
+ isNative?: boolean
123
+ contractAddress?: string
124
+ tokenMint?: string
125
+ }): string {
126
+ if (asset.isNative) return ''
127
+ if (asset.contractAddress) return ` contract \`${asset.contractAddress}\``
128
+ if (asset.tokenMint) return ` mint \`${asset.tokenMint}\``
129
+ return ''
130
+ }
131
+
132
+ function hashApprovalPayload(value: string): string {
133
+ return crypto.createHash('sha256').update(value).digest('hex')
134
+ }
135
+
136
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
137
+ return !!value && typeof value === 'object' && !Array.isArray(value)
138
+ }
139
+
140
+ function buildWalletApprovalComparableData(
141
+ category: ApprovalCategory,
142
+ action: string,
143
+ chain: 'ethereum' | 'solana',
144
+ data: Record<string, unknown>,
145
+ ): Record<string, unknown> | null {
146
+ return buildApprovalComparablePayload(category, {
147
+ action,
148
+ chain,
149
+ ...data,
150
+ })
151
+ }
152
+
153
+ async function requestWalletApproval(params: {
154
+ wallet: { requireApproval: boolean; chain: 'ethereum' | 'solana' }
155
+ approved: unknown
156
+ approvalId: unknown
157
+ category: ApprovalCategory
158
+ action: string
159
+ title: string
160
+ description: string
161
+ summary: string
162
+ data: Record<string, unknown>
163
+ context: { agentId?: string | null; sessionId?: string | null }
164
+ }): Promise<string | null> {
165
+ if (!params.wallet.requireApproval) return null
166
+ const requestedComparable = buildWalletApprovalComparableData(
167
+ params.category,
168
+ params.action,
169
+ params.wallet.chain,
170
+ params.data,
171
+ )
172
+ const requestFreshApproval = async (replacedApprovalId?: string): Promise<string | null> => {
173
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
174
+ const approval = await requestApprovalMaybeAutoApprove({
175
+ category: params.category,
176
+ title: params.title,
177
+ description: replacedApprovalId
178
+ ? `${params.description} Replacing stale approval ${replacedApprovalId} because the exact wallet action changed.`
179
+ : params.description,
180
+ data: {
181
+ action: params.action,
182
+ chain: params.wallet.chain,
183
+ summary: params.summary,
184
+ ...params.data,
185
+ },
186
+ agentId: params.context.agentId,
187
+ sessionId: params.context.sessionId,
188
+ })
189
+ if (approval.status === 'approved') return null
190
+ return JSON.stringify({
191
+ type: 'plugin_wallet_action_request',
192
+ approvalId: approval.id,
193
+ action: params.action,
194
+ chain: params.wallet.chain,
195
+ network: trimString(params.data.network),
196
+ summary: params.summary,
197
+ message: params.description,
198
+ replacesApprovalId: replacedApprovalId || undefined,
199
+ })
200
+ }
201
+
202
+ const requestedApprovalId = trimString(params.approvalId)
203
+ if (requestedApprovalId) {
204
+ const approvals = loadApprovals() as Record<string, ApprovalRequest>
205
+ const approval = approvals[requestedApprovalId]
206
+ if (!approval) {
207
+ return JSON.stringify({ error: `Approval "${requestedApprovalId}" was not found.` })
208
+ }
209
+ if (approval.category !== params.category) {
210
+ return JSON.stringify({ error: `Approval "${requestedApprovalId}" is not valid for ${params.category}.` })
211
+ }
212
+ if (params.context.agentId && approval.agentId && approval.agentId !== params.context.agentId) {
213
+ return JSON.stringify({ error: `Approval "${requestedApprovalId}" belongs to a different agent.` })
214
+ }
215
+ if (params.context.sessionId && approval.sessionId && approval.sessionId !== params.context.sessionId) {
216
+ return JSON.stringify({ error: `Approval "${requestedApprovalId}" belongs to a different session.` })
217
+ }
218
+ let approvalMatchesRequestedAction = true
219
+ if (params.category === 'wallet_action') {
220
+ if (trimString(approval.data.action) !== params.action) {
221
+ approvalMatchesRequestedAction = false
222
+ }
223
+ if (trimString(approval.data.chain) !== params.wallet.chain) {
224
+ approvalMatchesRequestedAction = false
225
+ }
226
+ const approvedComparable = buildWalletApprovalComparableData(
227
+ params.category,
228
+ params.action,
229
+ params.wallet.chain,
230
+ approval.data,
231
+ )
232
+ if (JSON.stringify(approvedComparable) !== JSON.stringify(requestedComparable)) {
233
+ approvalMatchesRequestedAction = false
234
+ }
235
+ }
236
+ if (params.category === 'wallet_transfer') {
237
+ const approvedComparable = buildWalletApprovalComparableData(
238
+ params.category,
239
+ params.action,
240
+ params.wallet.chain,
241
+ approval.data,
242
+ )
243
+ if (JSON.stringify(approvedComparable) !== JSON.stringify(requestedComparable)) {
244
+ approvalMatchesRequestedAction = false
245
+ }
246
+ }
247
+ if (!approvalMatchesRequestedAction) {
248
+ if (params.approved === true) {
249
+ return JSON.stringify({
250
+ error: `Approval "${requestedApprovalId}" does not match the requested wallet action. Omit approvalId or use the new approval the tool returns for the changed exact action.`,
251
+ })
252
+ }
253
+ return requestFreshApproval(requestedApprovalId)
254
+ }
255
+ if (approval.status === 'approved') return null
256
+ if (approval.status === 'pending') {
28
257
  return JSON.stringify({
29
- status: 'wallet_not_linked',
30
- message: 'No wallet linked to this agent yet.',
31
- setup: {
32
- endpoint: '/wallets',
33
- method: 'POST',
34
- body: { agentId, chain: 'solana' },
35
- },
258
+ type: params.category === 'wallet_transfer' ? 'plugin_wallet_transfer_request' : 'plugin_wallet_action_request',
259
+ approvalId: approval.id,
260
+ action: params.category === 'wallet_action' ? params.action : undefined,
261
+ chain: params.wallet.chain,
262
+ network: trimString(params.data.network),
263
+ summary: params.category === 'wallet_action' ? params.summary : undefined,
264
+ amount: params.category === 'wallet_transfer' ? params.data.amount : undefined,
265
+ amountDisplay: params.category === 'wallet_transfer' ? params.data.amountDisplay : undefined,
266
+ assetSymbol: params.category === 'wallet_transfer' ? params.data.assetSymbol : undefined,
267
+ toAddress: params.category === 'wallet_transfer' ? params.data.toAddress : undefined,
268
+ memo: params.category === 'wallet_transfer' ? params.data.memo : undefined,
269
+ message: params.description,
36
270
  })
37
271
  }
38
- return JSON.stringify({ error: 'No wallet linked to this agent. Ask the user to create one in the Wallets section.' })
272
+ return JSON.stringify({ error: `Approval "${requestedApprovalId}" was rejected.` })
273
+ }
274
+
275
+ if (params.approved === true) {
276
+ return JSON.stringify({
277
+ error: `Manual wallet approval must be linked with an approvalId for ${params.action}. Do not set approved:true without a real approval.`,
278
+ })
279
+ }
280
+ return requestFreshApproval()
281
+ }
282
+
283
+ function buildEthereumTransaction(normalized: Record<string, unknown>): {
284
+ transaction: Record<string, unknown>
285
+ summaryParts: string[]
286
+ } {
287
+ const explicitTx = parseRecordValue(normalized.transaction ?? normalized.transactionJson, 'transaction') || {}
288
+ const transaction: Record<string, unknown> = { ...explicitTx }
289
+ const summaryParts: string[] = []
290
+
291
+ const contractAddress = trimString(normalized.contractAddress)
292
+ const toAddress = trimString(normalized.toAddress ?? normalized.to)
293
+ if (!transaction.to && (contractAddress || toAddress)) {
294
+ transaction.to = contractAddress || toAddress
295
+ }
296
+
297
+ const data = trimString(normalized.data ?? normalized.calldata)
298
+ if (data) transaction.data = data
299
+
300
+ const valueAtomic = normalized.valueAtomic ?? normalized.valueWei
301
+ if (valueAtomic !== undefined && valueAtomic !== null && valueAtomic !== '') {
302
+ transaction.value = typeof valueAtomic === 'string' ? valueAtomic.trim() : valueAtomic
303
+ }
304
+
305
+ const abi = normalized.abi
306
+ const functionName = trimString(normalized.functionName)
307
+ if (abi !== undefined && functionName) {
308
+ const args = parseFunctionArgsValue(normalized.args ?? normalized.functionArgs, 'args') || []
309
+ const encoded = encodeEthereumContractCall(abi, functionName, args)
310
+ transaction.data = encoded.data
311
+ summaryParts.push(`contract call ${functionName}`)
312
+ if (contractAddress) summaryParts.push(`contract ${contractAddress}`)
313
+ }
314
+
315
+ if (trimString(String(transaction.to || ''))) summaryParts.push(`to ${String(transaction.to)}`)
316
+ if (typeof transaction.value === 'string' && transaction.value.trim()) {
317
+ summaryParts.push(`value ${transaction.value.trim()} wei`)
318
+ }
319
+ if (typeof transaction.data === 'string' && transaction.data.trim()) {
320
+ summaryParts.push(`data ${String(transaction.data).slice(0, 18)}...`)
39
321
  }
40
322
 
323
+ return { transaction, summaryParts }
324
+ }
325
+
326
+ function buildSolanaTransactionSummary(normalized: Record<string, unknown>, cluster: SolanaCluster): string {
327
+ const explicitTx = trimString(normalized.transactionBase64)
328
+ const signedTx = trimString(normalized.signedTransactionBase64)
329
+ const parts = [`cluster ${getSolanaClusterLabel(cluster)}`]
330
+ if (signedTx) parts.push('signed transaction')
331
+ else if (explicitTx) parts.push('unsigned transaction')
332
+ return parts.join(', ')
333
+ }
334
+
335
+ function buildWalletApprovalResumeInput(approval: ApprovalRequest): Record<string, unknown> | null {
336
+ const action = trimString(approval.data.action)
337
+ const chain = trimString(approval.data.chain)
338
+ const network = trimString(approval.data.network)
339
+ if (!action || !chain) return null
340
+
341
+ if (approval.category === 'wallet_transfer') {
342
+ const toAddress = trimString(approval.data.toAddress)
343
+ const amount = trimString(approval.data.amount)
344
+ const memo = trimString(approval.data.memo)
345
+ if (!toAddress || !amount) return null
346
+ return {
347
+ action: 'send',
348
+ chain,
349
+ toAddress,
350
+ amount,
351
+ ...(memo ? { memo } : {}),
352
+ }
353
+ }
354
+
355
+ if (approval.category !== 'wallet_action') return null
356
+
41
357
  switch (action) {
42
- case 'balance': {
358
+ case 'send_transaction':
359
+ case 'sign_transaction': {
360
+ const transaction = isPlainRecord(approval.data.transaction) ? approval.data.transaction : null
361
+ const signedTransaction = trimString(approval.data.signedTransaction)
362
+ if (!transaction && !signedTransaction) return null
363
+ return {
364
+ action,
365
+ chain,
366
+ ...(network ? { network } : {}),
367
+ ...(transaction ? { transaction } : {}),
368
+ ...(signedTransaction ? { signedTransaction } : {}),
369
+ }
370
+ }
371
+ case 'sign_typed_data': {
372
+ const domain = isPlainRecord(approval.data.domain) ? approval.data.domain : null
373
+ const types = isPlainRecord(approval.data.types) ? approval.data.types : null
374
+ const value = isPlainRecord(approval.data.value) ? approval.data.value : null
375
+ if (!domain || !types || !value) return null
376
+ return {
377
+ action,
378
+ chain,
379
+ ...(network ? { network } : {}),
380
+ domain,
381
+ types,
382
+ value,
383
+ }
384
+ }
385
+ case 'swap': {
386
+ const sellToken = trimString(approval.data.sellToken)
387
+ const buyToken = trimString(approval.data.buyToken)
388
+ const amountAtomic = trimString(approval.data.amountAtomic)
389
+ const recipient = trimString(approval.data.recipient)
390
+ const slippageBps = trimString(approval.data.slippageBps)
391
+ if (!sellToken || !buyToken || !amountAtomic) return null
392
+ return {
393
+ action,
394
+ chain,
395
+ ...(network ? { network } : {}),
396
+ sellToken,
397
+ buyToken,
398
+ sellAmountAtomic: amountAtomic,
399
+ ...(recipient ? { recipient } : {}),
400
+ ...(slippageBps ? { slippageBps } : {}),
401
+ }
402
+ }
403
+ default:
404
+ return null
405
+ }
406
+ }
407
+
408
+ async function executeWalletAction(args: unknown, context: { agentId?: string | null; sessionId?: string | null }) {
409
+ const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
410
+ const action = trimString(normalized.action) as WalletToolAction | ''
411
+ const requestedChainExplicit = normalized.chain !== undefined || normalized.provider !== undefined
412
+ const toAddress = trimString(normalized.toAddress ?? normalized.to)
413
+ const amount = normalized.amount as string | number | undefined
414
+ const amountLegacy = normalized.amountSol as number | undefined
415
+ const memo = trimString(normalized.memo)
416
+ const limit = typeof normalized.limit === 'number' ? normalized.limit : undefined
417
+ const label = trimString(normalized.label) || undefined
418
+ const agentId = context.agentId
419
+
420
+ if (!agentId) return JSON.stringify({ error: 'No agent ID in context' })
421
+
422
+ let requestedChain: 'ethereum' | 'solana'
423
+ try {
424
+ requestedChain = getWalletChainOrDefault(normalized.chain ?? normalized.provider, 'solana')
425
+ } catch (err: unknown) {
426
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
427
+ }
428
+
429
+ const wallets = getWalletsByAgentId(agentId)
430
+ const defaultWallet = getWalletByAgentId(agentId)
431
+ const requestedWallet = getWalletByAgentId(agentId, requestedChain)
432
+
433
+ if (wallets.length === 0) {
434
+ if (action === 'setup') {
43
435
  try {
44
- const { getBalance, lamportsToSol } = await import('../solana')
45
- const balanceLamports = await getBalance(wallet.publicKey)
46
- const sol = lamportsToSol(balanceLamports)
47
-
48
- // Return a Rich UI Card for balance
436
+ const created = createAgentWallet({ agentId, chain: requestedChain, label })
49
437
  return JSON.stringify({
50
- kind: 'plugin-ui',
51
- text: `### Wallet Balance\n\n**Address:** \`${wallet.publicKey}\`\n**Balance:** \`${sol} SOL\``,
438
+ status: 'wallet_created',
439
+ chain: created.chain,
440
+ address: created.publicKey,
441
+ symbol: getWalletAssetSymbol(created.chain),
442
+ message: `Created a ${created.chain} wallet for this agent.`,
52
443
  actions: [
53
- { id: 'view-solscan', label: 'View on Solscan', href: `https://solscan.io/account/${wallet.publicKey}` }
54
- ]
444
+ { id: 'view-wallet', label: 'Open Wallets', href: '/wallets' },
445
+ { id: 'view-explorer', label: 'View Address', href: getWalletExplorerUrl(created.chain, 'address', created.publicKey) },
446
+ ],
55
447
  })
56
448
  } catch (err: unknown) {
57
- return JSON.stringify({ error: `Failed to fetch balance: ${err instanceof Error ? err.message : String(err)}` })
449
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
58
450
  }
59
451
  }
60
- case 'address': return JSON.stringify({ address: wallet.publicKey, chain: wallet.chain })
61
- case 'send': {
62
- if (!toAddress) return JSON.stringify({ error: 'toAddress is required for send' })
63
- if (!amountSol || amountSol <= 0) return JSON.stringify({ error: 'amountSol must be positive' })
64
-
65
- if (normalized.approved !== true) {
66
- const { requestApprovalMaybeAutoApprove } = await import('../approvals')
67
- const approval = await requestApprovalMaybeAutoApprove({
68
- category: 'wallet_transfer',
69
- title: `Send ${amountSol} SOL`,
70
- description: `Transfer to ${toAddress}. Memo: ${memo || 'none'}`,
71
- data: { toAddress, amountSol, memo },
72
- agentId: context.agentId,
452
+
453
+ return JSON.stringify({
454
+ status: 'wallet_not_linked',
455
+ message: 'No wallet linked to this agent yet.',
456
+ setup: {
457
+ tool: 'wallet_tool',
458
+ action: 'setup',
459
+ body: { chain: requestedChain },
460
+ },
461
+ })
462
+ }
463
+
464
+ if (action === 'setup' && requestedChainExplicit && !requestedWallet) {
465
+ try {
466
+ const created = createAgentWallet({ agentId, chain: requestedChain, label })
467
+ return JSON.stringify({
468
+ status: 'wallet_created',
469
+ chain: created.chain,
470
+ address: created.publicKey,
471
+ symbol: getWalletAssetSymbol(created.chain),
472
+ message: `Created a ${created.chain} wallet for this agent.`,
473
+ actions: [
474
+ { id: 'view-wallet', label: 'Open Wallets', href: '/wallets' },
475
+ { id: 'view-explorer', label: 'View Address', href: getWalletExplorerUrl(created.chain, 'address', created.publicKey) },
476
+ ],
477
+ })
478
+ } catch (err: unknown) {
479
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
480
+ }
481
+ }
482
+
483
+ const wallet = requestedChainExplicit ? requestedWallet : defaultWallet
484
+ if (!wallet) {
485
+ return JSON.stringify({
486
+ status: 'wallet_not_linked',
487
+ message: requestedChainExplicit
488
+ ? `No ${requestedChain} wallet linked to this agent yet.`
489
+ : 'No wallet linked to this agent yet.',
490
+ setup: {
491
+ tool: 'wallet_tool',
492
+ action: 'setup',
493
+ body: { chain: requestedChain },
494
+ },
495
+ })
496
+ }
497
+
498
+ try {
499
+ switch (action) {
500
+ case 'setup': {
501
+ const activeWalletId = getAgentActiveWalletId(loadAgents()[agentId])
502
+ return JSON.stringify({
503
+ status: 'wallet_ready',
504
+ chain: wallet.chain,
505
+ address: wallet.publicKey,
506
+ symbol: getWalletAssetSymbol(wallet.chain),
507
+ isActive: activeWalletId === wallet.id,
508
+ message: requestedChainExplicit
509
+ ? `This agent already has a ${wallet.chain} wallet ready.`
510
+ : `This agent has ${wallets.length} wallet${wallets.length === 1 ? '' : 's'} linked. The default wallet is ${wallet.chain}.`,
511
+ })
512
+ }
513
+ case 'balance': {
514
+ const portfolio = await getWalletPortfolioSnapshot(wallet)
515
+ const assetLines = portfolio.assets
516
+ .filter((asset) => BigInt(asset.balanceAtomic) > BigInt(0))
517
+ .slice(0, 8)
518
+ .map((asset) => `- \`${asset.balanceDisplay}\` on \`${asset.networkLabel}\`${asset.isNative ? '' : ` via \`${asset.symbol}\``}${describeWalletAssetIdentity(asset)}`)
519
+ .join('\n')
520
+ return JSON.stringify({
521
+ kind: 'plugin-ui',
522
+ text: `### Wallet Balance\n\n**Chain:** \`${wallet.chain}\`\n**Address:** \`${wallet.publicKey}\`\n**Primary Balance:** \`${portfolio.balanceDisplay}\`\n**Assets Detected:** \`${portfolio.summary.nonZeroAssets}\`\n${assetLines ? `\n${assetLines}` : '\nNo funded assets detected yet.'}`,
523
+ actions: [
524
+ { id: 'view-wallet', label: 'View Address', href: getWalletExplorerUrl(wallet.chain, 'address', wallet.publicKey) },
525
+ ],
526
+ })
527
+ }
528
+ case 'address':
529
+ return JSON.stringify({
530
+ address: wallet.publicKey,
531
+ chain: wallet.chain,
532
+ symbol: getWalletAssetSymbol(wallet.chain),
533
+ explorerUrl: getWalletExplorerUrl(wallet.chain, 'address', wallet.publicKey),
73
534
  })
74
- if (approval.status !== 'approved') {
535
+ case 'send': {
536
+ const symbol = getWalletAssetSymbol(wallet.chain)
537
+ const displayAmount = amount ?? amountLegacy
538
+ if (!toAddress) return JSON.stringify({ error: 'toAddress is required for send' })
539
+ if (displayAmount === undefined || displayAmount === null || String(displayAmount).trim() === '') {
540
+ return JSON.stringify({ error: 'amount must be positive' })
541
+ }
542
+ if (!isValidWalletAddress(wallet.chain, toAddress)) return JSON.stringify({ error: `Invalid ${wallet.chain} address` })
543
+
544
+ let amountAtomic = '0'
545
+ let formattedAmount = ''
546
+ try {
547
+ amountAtomic = parseDisplayAmountToAtomic(displayAmount, wallet.chain === 'ethereum' ? 18 : 9)
548
+ if (BigInt(amountAtomic) <= BigInt(0)) return JSON.stringify({ error: 'amount must be positive' })
549
+ formattedAmount = formatWalletAmount(wallet.chain, amountAtomic, { minFractionDigits: 4, maxFractionDigits: 6 })
550
+ } catch (err: unknown) {
551
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
552
+ }
553
+
554
+ const perTxLimitAtomic = getWalletLimitAtomic(wallet, 'perTx')
555
+ if (BigInt(amountAtomic) > BigInt(perTxLimitAtomic)) {
75
556
  return JSON.stringify({
76
- type: 'plugin_wallet_transfer_request',
77
- amountSol,
78
- toAddress,
79
- memo,
80
- message: `I'm requesting to send ${amountSol} SOL to ${toAddress}. Please approve this transaction.`
557
+ error: `Amount ${formattedAmount} ${symbol} exceeds limit of ${formatWalletAmount(wallet.chain, perTxLimitAtomic, { maxFractionDigits: 6 })} ${symbol}`,
81
558
  })
82
559
  }
83
- normalized.approved = true
84
- }
85
560
 
86
- const { isValidSolanaAddress, solToLamports, lamportsToSol } = await import('../solana')
87
- if (!isValidSolanaAddress(toAddress)) return JSON.stringify({ error: 'Invalid Solana address' })
88
- const amountLamports = solToLamports(amountSol)
89
- const perTxLimit = wallet.spendingLimitLamports ?? 100_000_000
90
- if (amountLamports > perTxLimit) return JSON.stringify({ error: `Amount ${amountSol} SOL exceeds limit of ${lamportsToSol(perTxLimit)} SOL` })
91
- try {
561
+ const approvalResponse = await requestWalletApproval({
562
+ wallet,
563
+ approved: normalized.approved,
564
+ approvalId: normalized.approvalId,
565
+ category: 'wallet_transfer',
566
+ action,
567
+ title: `Send ${formattedAmount} ${symbol}`,
568
+ description: `Transfer to ${toAddress}. Memo: ${memo || 'none'}`,
569
+ summary: `transfer ${formattedAmount} ${symbol} to ${toAddress}`,
570
+ data: {
571
+ toAddress,
572
+ amount: formattedAmount,
573
+ amountDisplay: `${formattedAmount} ${symbol}`,
574
+ amountAtomic,
575
+ assetSymbol: symbol,
576
+ chain: wallet.chain,
577
+ memo,
578
+ },
579
+ context,
580
+ })
581
+ if (approvalResponse) return approvalResponse
582
+
92
583
  const baseUrl = process.env.NEXTAUTH_URL || `http://localhost:${process.env.PORT || 3456}`
93
584
  const res = await fetch(`${baseUrl}/api/wallets/${wallet.id}/send`, {
94
585
  method: 'POST',
95
586
  headers: { 'Content-Type': 'application/json', 'X-Access-Key': process.env.ACCESS_KEY || '' },
96
- body: JSON.stringify({ toAddress, amountLamports, memo }),
587
+ body: JSON.stringify({ toAddress, amountAtomic, memo }),
97
588
  })
98
589
  const data = await res.json()
99
-
590
+
100
591
  if (data.signature) {
101
592
  return JSON.stringify({
102
593
  kind: 'plugin-ui',
103
- text: `### Transaction Sent!\n\n**Amount:** \`${amountSol} SOL\`\n**To:** \`${toAddress}\`\n**Sig:** \`${data.signature.slice(0, 8)}...\``,
594
+ text: `### Transaction Sent!\n\n**Amount:** \`${formattedAmount} ${symbol}\`\n**To:** \`${toAddress}\`\n**Tx:** \`${data.signature.slice(0, 10)}...\``,
104
595
  actions: [
105
- { id: 'view-tx', label: 'View Transaction', href: `https://solscan.io/tx/${data.signature}` }
106
- ]
596
+ { id: 'view-tx', label: 'View Transaction', href: getWalletExplorerUrl(wallet.chain, 'transaction', data.signature) },
597
+ ],
107
598
  })
108
599
  }
109
600
  return JSON.stringify(data)
110
- } catch (err: unknown) {
111
- return JSON.stringify({ error: `Send failed: ${err instanceof Error ? err.message : String(err)}` })
112
- }
113
- }
114
- case 'transactions': {
115
- const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
116
- const walletTxs = Object.values(allTxs)
117
- .filter((tx) => tx.walletId === wallet.id)
118
- .sort((a, b) => b.timestamp - a.timestamp)
119
- .slice(0, limit ?? 5)
120
-
121
- const txLines = walletTxs.map(tx => `- **${tx.type.toUpperCase()}**: ${tx.amountLamports / 1e9} SOL (${tx.status})`).join('\n')
122
-
123
- return JSON.stringify({
124
- kind: 'plugin-ui',
125
- text: `### Recent Transactions\n\n${txLines || 'No recent transactions found.'}`,
126
- actions: [
127
- { id: 'view-history', label: 'Full History', href: `https://solscan.io/account/${wallet.publicKey}#transfers` }
128
- ]
129
- })
601
+ }
602
+ case 'transactions': {
603
+ const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
604
+ const walletTxs = Object.values(allTxs)
605
+ .filter((tx) => tx.walletId === wallet.id)
606
+ .sort((a, b) => b.timestamp - a.timestamp)
607
+ .slice(0, limit ?? 5)
608
+
609
+ const symbol = getWalletAssetSymbol(wallet.chain)
610
+ const txLines = walletTxs
611
+ .map((tx) => `- **${tx.type.toUpperCase()}**: ${formatWalletAmount(tx.chain, getWalletAtomicAmount(tx), { minFractionDigits: 4, maxFractionDigits: 6 })} ${symbol} (${tx.status})`)
612
+ .join('\n')
613
+
614
+ return JSON.stringify({
615
+ kind: 'plugin-ui',
616
+ text: `### Recent Transactions\n\n${txLines || 'No recent transactions found.'}`,
617
+ actions: [
618
+ { id: 'view-history', label: 'View Address', href: getWalletExplorerUrl(wallet.chain, 'address', wallet.publicKey) },
619
+ ],
620
+ })
621
+ }
622
+ case 'sign_message': {
623
+ const network = wallet.chain === 'ethereum'
624
+ ? getEvmNetworkConfig(normalized.network).label
625
+ : getSolanaClusterLabel(normalized.network)
626
+ const summary = `sign message on ${network}`
627
+ const approvalResponse = await requestWalletApproval({
628
+ wallet,
629
+ approved: normalized.approved,
630
+ approvalId: normalized.approvalId,
631
+ category: 'wallet_action',
632
+ action,
633
+ title: `Wallet action: sign message`,
634
+ description: `Sign a message with the ${wallet.chain} wallet on ${network}.`,
635
+ summary,
636
+ data: {
637
+ network: wallet.chain === 'ethereum'
638
+ ? getEvmNetworkConfig(normalized.network).id
639
+ : normalizeSolanaCluster(normalized.network),
640
+ messageDigest: hashApprovalPayload(JSON.stringify({
641
+ message: typeof normalized.message === 'string' ? normalized.message : null,
642
+ messageHex: typeof normalized.messageHex === 'string' ? normalized.messageHex : null,
643
+ messageBase64: typeof normalized.messageBase64 === 'string' ? normalized.messageBase64 : null,
644
+ })),
645
+ },
646
+ context,
647
+ })
648
+ if (approvalResponse) return approvalResponse
649
+
650
+ if (wallet.chain === 'ethereum') {
651
+ const result = await signEthereumMessage(wallet.encryptedPrivateKey, {
652
+ message: typeof normalized.message === 'string' ? normalized.message : null,
653
+ messageHex: typeof normalized.messageHex === 'string' ? normalized.messageHex : null,
654
+ messageBase64: typeof normalized.messageBase64 === 'string' ? normalized.messageBase64 : null,
655
+ })
656
+ return JSON.stringify({
657
+ status: 'signed',
658
+ action,
659
+ chain: wallet.chain,
660
+ network: getEvmNetworkConfig(normalized.network).id,
661
+ address: result.address,
662
+ signature: result.signature,
663
+ })
664
+ }
665
+
666
+ const result = await signSolanaMessage(wallet.encryptedPrivateKey, {
667
+ message: typeof normalized.message === 'string' ? normalized.message : null,
668
+ messageHex: typeof normalized.messageHex === 'string' ? normalized.messageHex : null,
669
+ messageBase64: typeof normalized.messageBase64 === 'string' ? normalized.messageBase64 : null,
670
+ })
671
+ return JSON.stringify({
672
+ status: 'signed',
673
+ action,
674
+ chain: wallet.chain,
675
+ network: normalizeSolanaCluster(normalized.network),
676
+ address: result.publicKey,
677
+ signature: result.signature,
678
+ })
679
+ }
680
+ case 'sign_typed_data': {
681
+ if (wallet.chain !== 'ethereum') {
682
+ return JSON.stringify({ error: 'sign_typed_data is only supported for Ethereum-compatible wallets' })
683
+ }
684
+
685
+ const typedData = parseRecordValue(normalized.typedData, 'typedData')
686
+ const domain = parseRecordValue(normalized.domain, 'domain') || parseRecordValue(typedData?.domain, 'typedData.domain')
687
+ const types = parseRecordValue(normalized.types, 'types') || parseRecordValue(typedData?.types, 'typedData.types')
688
+ const value = parseRecordValue(normalized.value, 'value')
689
+ || parseRecordValue(normalized.messageValue, 'messageValue')
690
+ || parseRecordValue(typedData?.message, 'typedData.message')
691
+ if (!domain || !types || !value) {
692
+ return JSON.stringify({ error: 'domain, types, and value are required for sign_typed_data' })
693
+ }
694
+
695
+ const network = getEvmNetworkConfig(normalized.network).id
696
+ const approvalResponse = await requestWalletApproval({
697
+ wallet,
698
+ approved: normalized.approved,
699
+ approvalId: normalized.approvalId,
700
+ category: 'wallet_action',
701
+ action,
702
+ title: 'Wallet action: sign typed data',
703
+ description: `Sign typed data with the Ethereum wallet on ${getEvmNetworkConfig(network).label}.`,
704
+ summary: `sign typed data on ${getEvmNetworkConfig(network).label}`,
705
+ data: { network, domain, types, value },
706
+ context,
707
+ })
708
+ if (approvalResponse) return approvalResponse
709
+
710
+ const result = await signEthereumTypedData(wallet.encryptedPrivateKey, { domain, types, value })
711
+ return JSON.stringify({
712
+ status: 'signed',
713
+ action,
714
+ chain: wallet.chain,
715
+ network,
716
+ address: result.address,
717
+ signature: result.signature,
718
+ })
719
+ }
720
+ case 'call_contract': {
721
+ if (wallet.chain !== 'ethereum') {
722
+ return JSON.stringify({ error: 'call_contract is only supported for Ethereum-compatible wallets' })
723
+ }
724
+ const abi = normalized.abi
725
+ const functionName = trimString(normalized.functionName)
726
+ const contractAddress = trimString(normalized.contractAddress)
727
+ if (!abi || !functionName || !contractAddress) {
728
+ return JSON.stringify({ error: 'contractAddress, abi, and functionName are required for call_contract' })
729
+ }
730
+ const args = parseFunctionArgsValue(normalized.args ?? normalized.functionArgs, 'args') || []
731
+ const result = await callEthereumContract(
732
+ wallet.encryptedPrivateKey,
733
+ {
734
+ contractAddress,
735
+ abi,
736
+ functionName,
737
+ args,
738
+ },
739
+ {
740
+ network: trimString(normalized.network) || undefined,
741
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
742
+ },
743
+ )
744
+ return JSON.stringify({
745
+ status: 'called',
746
+ action,
747
+ chain: wallet.chain,
748
+ network: result.network.id,
749
+ address: result.address,
750
+ contractAddress,
751
+ functionName,
752
+ fragment: result.fragment,
753
+ data: result.data,
754
+ rawResult: result.rawResult,
755
+ decoded: result.decoded,
756
+ namedOutputs: result.namedOutputs,
757
+ })
758
+ }
759
+ case 'encode_contract_call': {
760
+ if (wallet.chain !== 'ethereum') {
761
+ return JSON.stringify({ error: 'encode_contract_call is only supported for Ethereum-compatible wallets' })
762
+ }
763
+ const abi = normalized.abi
764
+ const functionName = trimString(normalized.functionName)
765
+ if (!abi || !functionName) return JSON.stringify({ error: 'abi and functionName are required for encode_contract_call' })
766
+ const args = parseFunctionArgsValue(normalized.args ?? normalized.functionArgs, 'args') || []
767
+ const encoded = encodeEthereumContractCall(abi, functionName, args)
768
+ return JSON.stringify({
769
+ status: 'encoded',
770
+ action,
771
+ chain: wallet.chain,
772
+ functionName,
773
+ fragment: encoded.fragment,
774
+ data: encoded.data,
775
+ })
776
+ }
777
+ case 'quote_swap': {
778
+ if (wallet.chain !== 'ethereum') {
779
+ return JSON.stringify({ error: 'quote_swap is only supported for Ethereum-compatible wallets' })
780
+ }
781
+ const network = getEvmNetworkConfig(normalized.network).id
782
+ const sellToken = trimString(pickFirstDefined(normalized, ['sellToken', 'fromToken', 'inputToken', 'srcToken', 'tokenIn']))
783
+ const buyToken = trimString(pickFirstDefined(normalized, ['buyToken', 'toToken', 'outputToken', 'destToken', 'tokenOut']))
784
+ const sellAmountAtomic = pickFirstDefined(normalized, ['sellAmountAtomic', 'amountAtomic', 'srcAmountAtomic'])
785
+ const sellAmountDisplay = pickFirstDefined(normalized, ['sellAmount', 'amount', 'sellAmountDisplay', 'srcAmount'])
786
+ const recipient = pickFirstDefined(normalized, ['recipient', 'receiver', 'destReceiver'])
787
+ const plan = await prepareEvmSwapPlan({
788
+ wallet,
789
+ network,
790
+ sellToken,
791
+ buyToken,
792
+ sellAmountAtomic,
793
+ sellAmountDisplay,
794
+ slippageBps: pickFirstDefined(normalized, ['slippageBps', 'slippage', 'slippagePercent']),
795
+ recipient,
796
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
797
+ })
798
+ return JSON.stringify({
799
+ status: 'quoted',
800
+ action,
801
+ chain: wallet.chain,
802
+ network,
803
+ provider: plan.provider,
804
+ routeSummary: plan.routeSummary,
805
+ sellToken: plan.sellToken,
806
+ buyToken: plan.buyToken,
807
+ sellAmountAtomic: plan.sellAmountAtomic,
808
+ sellAmountDisplay: plan.sellAmountDisplay,
809
+ estimatedBuyAmountAtomic: plan.buyAmountAtomic,
810
+ estimatedBuyAmountDisplay: plan.buyAmountDisplay,
811
+ slippageBps: plan.slippageBps,
812
+ spenderAddress: plan.spenderAddress,
813
+ approvalRequired: plan.approvalRequired,
814
+ approvalTransaction: plan.approvalTransaction,
815
+ swapTransaction: plan.swapTransaction,
816
+ recipient: plan.recipient,
817
+ })
818
+ }
819
+ case 'simulate_transaction': {
820
+ if (wallet.chain === 'ethereum') {
821
+ const network = getEvmNetworkConfig(normalized.network).id
822
+ const { transaction, summaryParts } = buildEthereumTransaction(normalized)
823
+ if (!transaction.to && !transaction.data) {
824
+ return JSON.stringify({ error: 'transaction.to or contract calldata is required for simulate_transaction' })
825
+ }
826
+ const result = await simulateEthereumTransaction(wallet.encryptedPrivateKey, transaction, {
827
+ network,
828
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
829
+ })
830
+ return JSON.stringify({
831
+ status: 'simulated',
832
+ action,
833
+ chain: wallet.chain,
834
+ network: result.network.id,
835
+ address: result.address,
836
+ estimateGas: result.estimateGas,
837
+ callResult: result.callResult,
838
+ callError: result.callError,
839
+ })
840
+ }
841
+
842
+ const cluster = normalizeSolanaCluster(normalized.network)
843
+ const transactionBase64 = trimString(normalized.transactionBase64)
844
+ if (!transactionBase64) return JSON.stringify({ error: 'transactionBase64 is required for Solana simulate_transaction' })
845
+ const result = await simulateSolanaTransaction(wallet.encryptedPrivateKey, transactionBase64, {
846
+ cluster,
847
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
848
+ })
849
+ return JSON.stringify({
850
+ status: 'simulated',
851
+ action,
852
+ chain: wallet.chain,
853
+ network: cluster,
854
+ address: result.publicKey,
855
+ signatures: result.signatures,
856
+ logs: result.logs,
857
+ unitsConsumed: result.unitsConsumed,
858
+ err: result.err,
859
+ versioned: result.versioned,
860
+ })
861
+ }
862
+ case 'sign_transaction': {
863
+ if (wallet.chain === 'ethereum') {
864
+ const network = getEvmNetworkConfig(normalized.network).id
865
+ const { transaction, summaryParts } = buildEthereumTransaction(normalized)
866
+ if (!transaction.to && !transaction.data) {
867
+ return JSON.stringify({ error: 'transaction.to or contract calldata is required for sign_transaction' })
868
+ }
869
+ const approvalResponse = await requestWalletApproval({
870
+ wallet,
871
+ approved: normalized.approved,
872
+ approvalId: normalized.approvalId,
873
+ category: 'wallet_action',
874
+ action,
875
+ title: 'Wallet action: sign transaction',
876
+ description: `Sign an Ethereum transaction on ${getEvmNetworkConfig(network).label}.`,
877
+ summary: summaryParts.join(', ') || `sign transaction on ${getEvmNetworkConfig(network).label}`,
878
+ data: { network, transaction },
879
+ context,
880
+ })
881
+ if (approvalResponse) return approvalResponse
882
+
883
+ const result = await signEthereumTransaction(wallet.encryptedPrivateKey, transaction, {
884
+ network,
885
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
886
+ })
887
+ return JSON.stringify({
888
+ status: 'signed',
889
+ action,
890
+ chain: wallet.chain,
891
+ network: result.network.id,
892
+ address: result.address,
893
+ signedTransaction: result.signedTransaction,
894
+ transactionHash: result.transactionHash,
895
+ })
896
+ }
897
+
898
+ const transactionBase64 = trimString(normalized.transactionBase64)
899
+ if (!transactionBase64) return JSON.stringify({ error: 'transactionBase64 is required for Solana sign_transaction' })
900
+ const cluster = normalizeSolanaCluster(normalized.network)
901
+ const approvalResponse = await requestWalletApproval({
902
+ wallet,
903
+ approved: normalized.approved,
904
+ approvalId: normalized.approvalId,
905
+ category: 'wallet_action',
906
+ action,
907
+ title: 'Wallet action: sign transaction',
908
+ description: `Sign a Solana transaction on ${getSolanaClusterLabel(cluster)}.`,
909
+ summary: buildSolanaTransactionSummary(normalized, cluster),
910
+ data: {
911
+ network: cluster,
912
+ transactionFingerprint: hashApprovalPayload(transactionBase64),
913
+ },
914
+ context,
915
+ })
916
+ if (approvalResponse) return approvalResponse
917
+
918
+ const result = await signSolanaTransaction(wallet.encryptedPrivateKey, transactionBase64)
919
+ return JSON.stringify({
920
+ status: 'signed',
921
+ action,
922
+ chain: wallet.chain,
923
+ network: cluster,
924
+ address: result.publicKey,
925
+ signatures: result.signatures,
926
+ signedTransactionBase64: result.signedTransactionBase64,
927
+ versioned: result.versioned,
928
+ })
929
+ }
930
+ case 'swap': {
931
+ if (wallet.chain !== 'ethereum') {
932
+ return JSON.stringify({ error: 'swap is only supported for Ethereum-compatible wallets' })
933
+ }
934
+ const network = getEvmNetworkConfig(normalized.network).id
935
+ const sellToken = trimString(pickFirstDefined(normalized, ['sellToken', 'fromToken', 'inputToken', 'srcToken', 'tokenIn']))
936
+ const buyToken = trimString(pickFirstDefined(normalized, ['buyToken', 'toToken', 'outputToken', 'destToken', 'tokenOut']))
937
+ const sellAmountAtomic = pickFirstDefined(normalized, ['sellAmountAtomic', 'amountAtomic', 'srcAmountAtomic'])
938
+ const sellAmountDisplay = pickFirstDefined(normalized, ['sellAmount', 'amount', 'sellAmountDisplay', 'srcAmount'])
939
+ const recipient = pickFirstDefined(normalized, ['recipient', 'receiver', 'destReceiver'])
940
+ const slippageBps = pickFirstDefined(normalized, ['slippageBps', 'slippage', 'slippagePercent'])
941
+ const waitForReceipt = normalized.waitForReceipt !== false
942
+ const buildPlan = () => prepareEvmSwapPlan({
943
+ wallet,
944
+ network,
945
+ sellToken,
946
+ buyToken,
947
+ sellAmountAtomic,
948
+ sellAmountDisplay,
949
+ slippageBps,
950
+ recipient,
951
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
952
+ })
953
+ let plan = await buildPlan()
954
+ const title = `Wallet action: swap ${plan.sellAmountDisplay} to ${plan.buyToken.symbol}`
955
+ const description = plan.approvalRequired
956
+ ? `Execute a swap on ${plan.network.label}. This will send an exact token approval transaction to ${plan.spenderAddress} and then broadcast the swap transaction.`
957
+ : `Execute a swap on ${plan.network.label} and broadcast the resulting transaction.`
958
+ const summary = `swap ${plan.sellAmountDisplay} for about ${plan.buyAmountDisplay} on ${plan.network.label} via ${plan.provider}${plan.routeSummary ? ` (${plan.routeSummary})` : ''}`
959
+ const approvalResponse = await requestWalletApproval({
960
+ wallet,
961
+ approved: normalized.approved,
962
+ approvalId: normalized.approvalId,
963
+ category: 'wallet_action',
964
+ action,
965
+ title,
966
+ description,
967
+ summary,
968
+ data: {
969
+ network,
970
+ sellToken: plan.sellToken.address,
971
+ buyToken: plan.buyToken.address,
972
+ recipient: plan.recipient,
973
+ amountAtomic: plan.sellAmountAtomic,
974
+ amountDisplay: plan.sellAmountDisplay,
975
+ estimatedBuyAmountAtomic: plan.buyAmountAtomic,
976
+ estimatedBuyAmountDisplay: plan.buyAmountDisplay,
977
+ slippageBps: String(plan.slippageBps),
978
+ routeProvider: plan.provider,
979
+ routeSummary: plan.routeSummary,
980
+ spenderAddress: plan.spenderAddress || '',
981
+ },
982
+ context,
983
+ })
984
+ if (approvalResponse) return approvalResponse
985
+
986
+ let approvalBroadcast: Awaited<ReturnType<typeof sendEthereumTransaction>> | null = null
987
+ if (plan.approvalRequired && plan.approvalTransaction) {
988
+ approvalBroadcast = await sendEthereumTransaction(
989
+ wallet.encryptedPrivateKey,
990
+ {
991
+ transaction: plan.approvalTransaction,
992
+ waitForReceipt: true,
993
+ },
994
+ {
995
+ network,
996
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
997
+ },
998
+ )
999
+ clearWalletPortfolioCache(wallet.id)
1000
+ }
1001
+
1002
+ const sendSwap = async (preparedPlan: typeof plan) => sendEthereumTransaction(
1003
+ wallet.encryptedPrivateKey,
1004
+ {
1005
+ transaction: preparedPlan.swapTransaction,
1006
+ waitForReceipt,
1007
+ },
1008
+ {
1009
+ network,
1010
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
1011
+ },
1012
+ )
1013
+
1014
+ let swapResult: Awaited<ReturnType<typeof sendEthereumTransaction>>
1015
+ try {
1016
+ swapResult = await sendSwap(plan)
1017
+ } catch (err: unknown) {
1018
+ if (!isLikelyRetryableSwapError(err)) throw err
1019
+ plan = await buildPlan()
1020
+ swapResult = await sendSwap(plan)
1021
+ }
1022
+
1023
+ clearWalletPortfolioCache(wallet.id)
1024
+ const txId = genId()
1025
+ const now = Date.now()
1026
+ const approvedBy = wallet.requireApproval ? (trimString(normalized.approvalId) ? 'user' : 'auto') : undefined
1027
+ const txRecord: WalletTransaction = {
1028
+ id: txId,
1029
+ walletId: wallet.id,
1030
+ agentId,
1031
+ chain: wallet.chain,
1032
+ type: 'swap',
1033
+ signature: swapResult.transactionHash,
1034
+ fromAddress: wallet.publicKey,
1035
+ toAddress: plan.recipient,
1036
+ amountAtomic: plan.sellAmountAtomic,
1037
+ feeAtomic: typeof swapResult.receipt?.fee === 'string' ? swapResult.receipt.fee : undefined,
1038
+ status: swapResult.receipt ? 'confirmed' : 'pending',
1039
+ memo: `Swapped ${plan.sellAmountDisplay} to approximately ${plan.buyAmountDisplay} on ${plan.network.label}`,
1040
+ approvedBy,
1041
+ tokenMint: plan.sellToken.isNative ? undefined : plan.sellToken.address,
1042
+ timestamp: now,
1043
+ }
1044
+ upsertWalletTransaction(txId, txRecord)
1045
+
1046
+ return JSON.stringify({
1047
+ status: swapResult.receipt ? 'confirmed' : 'broadcast',
1048
+ action,
1049
+ chain: wallet.chain,
1050
+ network,
1051
+ provider: plan.provider,
1052
+ routeSummary: plan.routeSummary,
1053
+ sellToken: plan.sellToken,
1054
+ buyToken: plan.buyToken,
1055
+ sellAmountAtomic: plan.sellAmountAtomic,
1056
+ sellAmountDisplay: plan.sellAmountDisplay,
1057
+ estimatedBuyAmountAtomic: plan.buyAmountAtomic,
1058
+ estimatedBuyAmountDisplay: plan.buyAmountDisplay,
1059
+ approvalTransactionHash: approvalBroadcast?.transactionHash || undefined,
1060
+ transactionHash: swapResult.transactionHash,
1061
+ explorerUrl: swapResult.explorerUrl,
1062
+ receipt: swapResult.receipt,
1063
+ recipient: plan.recipient,
1064
+ })
1065
+ }
1066
+ case 'send_transaction': {
1067
+ if (wallet.chain === 'ethereum') {
1068
+ const network = getEvmNetworkConfig(normalized.network).id
1069
+ const signedTransaction = trimString(normalized.signedTransaction)
1070
+ const { transaction, summaryParts } = buildEthereumTransaction(normalized)
1071
+ if (!signedTransaction && !transaction.to && !transaction.data) {
1072
+ return JSON.stringify({ error: 'signedTransaction, transaction.to, or contract calldata is required for send_transaction' })
1073
+ }
1074
+
1075
+ const approvalResponse = await requestWalletApproval({
1076
+ wallet,
1077
+ approved: normalized.approved,
1078
+ approvalId: normalized.approvalId,
1079
+ category: 'wallet_action',
1080
+ action,
1081
+ title: 'Wallet action: send transaction',
1082
+ description: `Broadcast an Ethereum transaction on ${getEvmNetworkConfig(network).label}.`,
1083
+ summary: summaryParts.join(', ') || `broadcast transaction on ${getEvmNetworkConfig(network).label}`,
1084
+ data: {
1085
+ network,
1086
+ transaction,
1087
+ signedTransactionFingerprint: signedTransaction ? hashApprovalPayload(signedTransaction) : '',
1088
+ },
1089
+ context,
1090
+ })
1091
+ if (approvalResponse) return approvalResponse
1092
+
1093
+ const result = await sendEthereumTransaction(
1094
+ wallet.encryptedPrivateKey,
1095
+ {
1096
+ transaction: signedTransaction ? undefined : transaction,
1097
+ signedTransaction: signedTransaction || undefined,
1098
+ waitForReceipt: normalized.waitForReceipt === true,
1099
+ },
1100
+ {
1101
+ network,
1102
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
1103
+ },
1104
+ )
1105
+ clearWalletPortfolioCache(wallet.id)
1106
+ return JSON.stringify({
1107
+ status: 'broadcast',
1108
+ action,
1109
+ chain: wallet.chain,
1110
+ network: result.network.id,
1111
+ address: result.address,
1112
+ transactionHash: result.transactionHash,
1113
+ explorerUrl: result.explorerUrl,
1114
+ receipt: result.receipt,
1115
+ })
1116
+ }
1117
+
1118
+ const cluster = normalizeSolanaCluster(normalized.network)
1119
+ const transactionBase64 = trimString(normalized.transactionBase64)
1120
+ const signedTransactionBase64 = trimString(normalized.signedTransactionBase64)
1121
+ if (!transactionBase64 && !signedTransactionBase64) {
1122
+ return JSON.stringify({ error: 'transactionBase64 or signedTransactionBase64 is required for Solana send_transaction' })
1123
+ }
1124
+ const approvalResponse = await requestWalletApproval({
1125
+ wallet,
1126
+ approved: normalized.approved,
1127
+ approvalId: normalized.approvalId,
1128
+ category: 'wallet_action',
1129
+ action,
1130
+ title: 'Wallet action: send transaction',
1131
+ description: `Broadcast a Solana transaction on ${getSolanaClusterLabel(cluster)}.`,
1132
+ summary: buildSolanaTransactionSummary(normalized, cluster),
1133
+ data: {
1134
+ network: cluster,
1135
+ transactionFingerprint: transactionBase64 ? hashApprovalPayload(transactionBase64) : '',
1136
+ signedTransactionFingerprint: signedTransactionBase64 ? hashApprovalPayload(signedTransactionBase64) : '',
1137
+ },
1138
+ context,
1139
+ })
1140
+ if (approvalResponse) return approvalResponse
1141
+
1142
+ const result = await sendSolanaTransaction(
1143
+ wallet.encryptedPrivateKey,
1144
+ {
1145
+ transactionBase64: transactionBase64 || undefined,
1146
+ signedTransactionBase64: signedTransactionBase64 || undefined,
1147
+ waitForConfirmation: normalized.waitForConfirmation !== false,
1148
+ },
1149
+ {
1150
+ cluster,
1151
+ rpcUrl: trimString(normalized.rpcUrl) || undefined,
1152
+ },
1153
+ )
1154
+ clearWalletPortfolioCache(wallet.id)
1155
+ return JSON.stringify({
1156
+ status: 'broadcast',
1157
+ action,
1158
+ chain: wallet.chain,
1159
+ network: cluster,
1160
+ address: result.publicKey,
1161
+ signature: result.signature,
1162
+ explorerUrl: result.explorerUrl,
1163
+ versioned: result.versioned,
1164
+ })
1165
+ }
1166
+ default:
1167
+ return JSON.stringify({ error: `Unknown action: ${action}` })
130
1168
  }
131
- default: return JSON.stringify({ error: `Unknown action: ${action}` })
1169
+ } catch (err: unknown) {
1170
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
132
1171
  }
133
1172
  }
134
1173
 
135
- /**
136
- * Register as a Built-in Plugin with UI extensions
137
- */
138
1174
  const WalletPlugin: Plugin = {
139
1175
  name: 'Core Wallet',
140
- description: 'Manage agent crypto wallet: check balance, send SOL, view transactions.',
1176
+ description: 'Manage an agent wallet, inspect assets, sign payloads, and execute generic onchain actions without venue-specific adapters.',
141
1177
  hooks: {
142
1178
  getAgentContext: async (ctx) => {
143
1179
  const agentId = ctx.session.agentId
144
1180
  if (!agentId) return null
145
- const { loadWallets } = await import('../storage')
146
- const wallets = loadWallets() as Record<string, { agentId: string; publicKey: string; chain: string; spendingLimitLamports?: number; dailyLimitLamports?: number; requireApproval: boolean }>
147
- const wallet = Object.values(wallets).find((w) => w.agentId === agentId)
148
- if (!wallet) return null
149
- const { getBalance, lamportsToSol } = await import('../solana')
150
- let balanceSol = 0
151
- try {
152
- const lamports = await getBalance(wallet.publicKey)
153
- balanceSol = lamportsToSol(lamports)
154
- } catch { /* RPC failure show 0 */ }
155
- const perTxLimit = lamportsToSol(wallet.spendingLimitLamports ?? 100_000_000)
156
- const dailyLimit = lamportsToSol(wallet.dailyLimitLamports ?? 1_000_000_000)
157
- return [
158
- '## Your Wallet',
159
- `You own a ${wallet.chain} wallet — this is your wallet, your funds, your address. Speak about it in the first person ("my wallet", "my balance", "I have X SOL"). You can spend from it freely within your limits${wallet.requireApproval ? ', though the user needs to approve each send before it goes through' : ''}.`,
160
- `- Address: ${wallet.publicKey}`,
161
- `- Balance: ${balanceSol} SOL`,
162
- `- Per-transaction limit: ${perTxLimit} SOL`,
163
- `- Daily limit: ${dailyLimit} SOL`,
164
- 'Use the `wallet_tool` to check your balance, send SOL, or view your transaction history.',
165
- ].join('\n')
1181
+ const wallets = getWalletsByAgentId(agentId)
1182
+ if (wallets.length === 0) return null
1183
+
1184
+ const agent = loadAgents()[agentId]
1185
+ const activeWalletId = getAgentActiveWalletId(agent)
1186
+ const onlyWallet = wallets[0]
1187
+ const lines = [
1188
+ wallets.length === 1 ? '## Your Wallet' : '## Your Wallets',
1189
+ wallets.length === 1
1190
+ ? `You own a ${onlyWallet?.chain || 'wallet'} wallet. Speak about it in the first person ("my wallet", "my balance"). You can inspect assets, sign messages, sign transactions, and submit generic onchain actions${onlyWallet?.requireApproval ? ', but the user may still need to approve risky actions' : ''}.`
1191
+ : `You own ${wallets.length} wallets across multiple chains. Speak about them in the first person ("my wallet", "my Ethereum wallet", "my Solana wallet"). If you need a specific wallet, use the \`chain\` parameter on \`wallet_tool\` actions.`,
1192
+ ]
1193
+
1194
+ for (const entry of wallets) {
1195
+ let portfolio = {
1196
+ balanceAtomic: '0',
1197
+ balanceDisplay: `0.0000 ${getWalletAssetSymbol(entry.chain)}`,
1198
+ assets: [] as Array<{ balanceAtomic: string; balanceDisplay?: string; networkLabel: string; symbol: string; isNative?: boolean }>,
1199
+ summary: { totalAssets: 0, nonZeroAssets: 0, tokenAssets: 0, networkCount: 0 },
1200
+ }
1201
+ try {
1202
+ portfolio = await getWalletPortfolioSnapshot(entry)
1203
+ } catch {
1204
+ // best-effort context only
1205
+ }
1206
+ const symbol = getWalletAssetSymbol(entry.chain)
1207
+ const perTxLimit = formatWalletAmount(entry.chain, getWalletLimitAtomic(entry, 'perTx'), { maxFractionDigits: 6 })
1208
+ const dailyLimit = formatWalletAmount(entry.chain, getWalletLimitAtomic(entry, 'daily'), { maxFractionDigits: 6 })
1209
+ const tokenPreview = portfolio.assets
1210
+ .filter((asset) => BigInt(asset.balanceAtomic) > BigInt(0) && asset.isNative !== true)
1211
+ .slice(0, 2)
1212
+ .map((asset) => `${asset.balanceDisplay || `${asset.symbol} on ${asset.networkLabel}`}${describeWalletAssetIdentity(asset)}`)
1213
+ .join(', ')
1214
+ lines.push(
1215
+ `- ${entry.chain === 'ethereum' ? 'Ethereum' : 'Solana'}${entry.id === activeWalletId ? ' (default)' : ''}: ${portfolio.balanceDisplay} at ${entry.publicKey}`,
1216
+ tokenPreview ? ` Tokens: ${tokenPreview}` : ` Assets detected: ${portfolio.summary.nonZeroAssets}`,
1217
+ ` Limits: ${perTxLimit} ${symbol}/tx, ${dailyLimit} ${symbol}/day${entry.requireApproval ? ', approval required' : ', auto-execution enabled'}`,
1218
+ )
1219
+ }
1220
+
1221
+ lines.push('Use `wallet_tool` to inspect balances before external-service work. For API-native integrations, pair `wallet_tool` with `http_request`: fetch the docs or API payload first, then sign or broadcast only the specific request you can justify.')
1222
+ lines.push('For standard EVM token swaps on supported networks, prefer `wallet_tool` action `swap` or `quote_swap` instead of manually assembling router calldata from public APIs.')
1223
+ lines.push('When public quote or aggregator APIs are inconsistent, use read-only onchain primitives instead of endless venue-shopping. `wallet_tool` action `call_contract` can query allowances, quotes, and protocol state directly on EVM networks.')
1224
+ lines.push('Treat contract addresses, token mints, router addresses, and spender addresses returned by wallet or HTTP tools as authoritative inputs. Do not invent replacements unless a later tool result proves the earlier value is wrong.')
1225
+ lines.push('For EVM work, set `network` to `ethereum`, `arbitrum`, or `base`. For Solana work, set `network` to `mainnet-beta`, `devnet`, or `testnet`.')
1226
+ return lines.join('\n')
1227
+ },
1228
+ getCapabilityDescription: () => 'I can use my wallets through `wallet_tool` for setup, balance checks, address inspection, native transfers, read-only contract calls, message signing, typed-data signing, calldata encoding, generic EVM token swap quotes/execution, transaction simulation, and raw transaction broadcast.',
1229
+ getOperatingGuidance: () => [
1230
+ 'Use `wallet_tool` to inspect balances and select the right wallet before exploring an external service or trading venue.',
1231
+ 'For a standard EVM DEX trade on Ethereum, Arbitrum, or Base, prefer `wallet_tool` action `swap` before trying to invent router calldata yourself.',
1232
+ 'Use `wallet_tool` action `quote_swap` when you need a read-only preview of the spender, allowance requirement, route, and executable swap transaction.',
1233
+ 'Pair `wallet_tool` with `http_request` for API-native exchange and dApp workflows: discover the docs or payload first, then sign only the exact message or transaction required.',
1234
+ 'Use the browser only for rendered UI or interactive page steps after the required wallet action is already understood.',
1235
+ 'If multiple public APIs fail, switch to direct read-only contract calls with `wallet_tool` action `call_contract` instead of continuing to shop for venues.',
1236
+ 'Treat token addresses, token mints, router addresses, spender addresses, and network identifiers returned by tools as authoritative unless newer tool evidence proves they changed.',
1237
+ 'A prose approval request does not count. When the next step is a signature, approval, or transaction, call the real `wallet_tool` action so the runtime can create the exact approval boundary.',
1238
+ 'Pass `chain: "ethereum"` or `chain: "solana"` explicitly whenever the task depends on a specific wallet.',
1239
+ 'For EVM actions, also pass `network: "ethereum"`, `"arbitrum"`, or `"base"` when the venue or asset lives on a specific network.',
1240
+ 'Treat `wallet_tool` as a server-side wallet capability. It does not inject a browser wallet extension or click wallet-connect/signature prompts inside third-party UIs.',
1241
+ ],
1242
+ getApprovalGuidance: ({ approval, phase, approved }) => {
1243
+ const category = approval.category
1244
+ if (category !== 'wallet_action' && category !== 'wallet_transfer') return null
1245
+
1246
+ const resumeInput = buildWalletApprovalResumeInput(approval)
1247
+ if (phase === 'request') {
1248
+ return [
1249
+ 'When this approval is granted, continue by calling `wallet_tool` for the exact approved action. Do not ask for approval again in prose.',
1250
+ 'Do not change the approved amount, route, spender, destination, contract, or network unless a later tool result proves the approved action cannot execute as approved.',
1251
+ ]
1252
+ }
1253
+
1254
+ if (phase === 'connector_reminder') {
1255
+ return 'Approving this lets the agent resume the exact blocked wallet action automatically.'
1256
+ }
1257
+
1258
+ if (approved !== true) {
1259
+ return 'Do not retry the rejected wallet action. Inspect state again and request a fresh approval only for a materially different exact action justified by tool evidence.'
1260
+ }
1261
+
1262
+ const lines = [
1263
+ 'Resume immediately with `wallet_tool` using the exact approved action and this approvalId.',
1264
+ 'Do not re-quote, re-route, or browse for alternatives before attempting the approved wallet action once.',
1265
+ ]
1266
+ if (resumeInput) {
1267
+ lines.push(`Exact tool input: ${JSON.stringify({ ...resumeInput, approvalId: approval.id })}`)
1268
+ } else {
1269
+ lines.push('Use the `Approved payload` fields above as the exact wallet action inputs and add the approvalId.')
1270
+ }
1271
+ return lines
166
1272
  },
167
- getCapabilityDescription: () => 'I have my own crypto wallet (`wallet_tool`) — I can check my balance, send SOL, and review my transaction history.',
168
1273
  } as PluginHooks,
169
1274
  ui: {
170
1275
  sidebarItems: [
@@ -172,59 +1277,189 @@ const WalletPlugin: Plugin = {
172
1277
  id: 'wallet-dashboard',
173
1278
  label: 'Wallet',
174
1279
  href: '/wallets',
175
- position: 'top'
176
- }
1280
+ position: 'top',
1281
+ },
177
1282
  ],
178
1283
  headerWidgets: [
179
1284
  {
180
1285
  id: 'wallet-status',
181
- label: 'Wallet'
182
- }
183
- ]
1286
+ label: 'Wallet',
1287
+ },
1288
+ ],
184
1289
  },
185
1290
  tools: [
186
1291
  {
187
1292
  name: 'wallet_tool',
188
- description: 'Manage your own crypto wallet.',
1293
+ description: 'Manage your own crypto wallet, including setup, balances, read-only contract calls, signatures, generic EVM swap execution, transaction simulation, and generic onchain execution.',
1294
+ planning: {
1295
+ capabilities: [TOOL_CAPABILITY.walletInspect, TOOL_CAPABILITY.walletExecute],
1296
+ disciplineGuidance: [
1297
+ 'For `wallet_tool`, inspect balances or addresses before attempting an exchange, dApp, or onchain workflow.',
1298
+ 'Pass `{"chain":"ethereum"}` or `{"chain":"solana"}` explicitly whenever the task depends on a specific wallet.',
1299
+ 'For a standard EVM token trade on Ethereum, Arbitrum, or Base, prefer `{"action":"swap",...}` or `{"action":"quote_swap",...}` instead of manually assembling router calldata.',
1300
+ 'For API-native workflows, fetch the docs or request payload first, then use `wallet_tool` only for the exact signature, simulation, or broadcast step.',
1301
+ 'If quote or assembly APIs keep failing, stop venue-shopping and use `wallet_tool` action `call_contract` for direct read-only onchain state or quote reads.',
1302
+ 'Treat `wallet_tool` as a server-side wallet capability. It does not inject a browser wallet extension or complete third-party wallet-connect prompts for you.',
1303
+ ],
1304
+ requestMatchers: [
1305
+ {
1306
+ capability: TOOL_CAPABILITY.walletInspect,
1307
+ patterns: [
1308
+ 'wallet',
1309
+ 'balance',
1310
+ 'address',
1311
+ 'fund',
1312
+ 'transfer',
1313
+ 'send',
1314
+ 'deposit',
1315
+ 'withdraw',
1316
+ 'swap',
1317
+ 'bridge',
1318
+ 'onchain',
1319
+ 'token',
1320
+ 'gas',
1321
+ 'usdc',
1322
+ 'eth',
1323
+ 'sol',
1324
+ 'solana',
1325
+ 'ethereum',
1326
+ 'arbitrum',
1327
+ 'base',
1328
+ 'wallet connect',
1329
+ 'walletconnect',
1330
+ 'dex',
1331
+ 'erc-20',
1332
+ 'spl',
1333
+ 'trade on',
1334
+ 'quote swap',
1335
+ ],
1336
+ },
1337
+ {
1338
+ capability: TOOL_CAPABILITY.walletExecute,
1339
+ patterns: [
1340
+ 'swap',
1341
+ 'trade',
1342
+ 'buy token',
1343
+ 'sell token',
1344
+ 'sign message',
1345
+ 'sign typed data',
1346
+ 'signature',
1347
+ 'typed data',
1348
+ 'eip-712',
1349
+ 'sign transaction',
1350
+ 'send transaction',
1351
+ 'simulate transaction',
1352
+ 'broadcast transaction',
1353
+ 'contract call',
1354
+ 'call contract',
1355
+ 'read contract',
1356
+ 'calldata',
1357
+ 'approve token',
1358
+ 'raw transaction',
1359
+ ],
1360
+ },
1361
+ ],
1362
+ },
189
1363
  parameters: {
190
1364
  type: 'object',
191
1365
  properties: {
192
- action: { type: 'string', enum: ['balance', 'address', 'send', 'transactions'] },
1366
+ action: { type: 'string', enum: [...WALLET_TOOL_ACTIONS] },
1367
+ chain: { type: 'string', enum: ['solana', 'ethereum'], description: 'Selects or creates the wallet on this chain.' },
1368
+ provider: { type: 'string', description: 'Alias for chain or wallet ecosystem, for example "ethereum" or "evm".' },
1369
+ network: { type: 'string', description: 'Execution network or cluster. EVM: ethereum/arbitrum/base. Solana: mainnet-beta/devnet/testnet.' },
1370
+ rpcUrl: { type: 'string', description: 'Optional RPC override for the selected network.' },
1371
+ label: { type: 'string' },
193
1372
  toAddress: { type: 'string' },
1373
+ contractAddress: { type: 'string' },
1374
+ amount: { type: 'string', description: 'Native asset amount in display units, such as SOL or ETH.' },
194
1375
  amountSol: { type: 'number' },
195
1376
  memo: { type: 'string' },
196
1377
  limit: { type: 'number' },
197
- approved: { type: 'boolean', description: 'Set to true only after user has manually approved the transfer request.' }
1378
+ message: { type: 'string' },
1379
+ messageHex: { type: 'string' },
1380
+ messageBase64: { type: 'string' },
1381
+ abi: { type: 'string', description: 'ABI JSON array or a single ABI fragment string.' },
1382
+ functionName: { type: 'string' },
1383
+ args: { type: 'string', description: 'JSON array of function arguments, or a JSON object keyed by ABI input names.' },
1384
+ sellToken: { type: 'string', description: 'For EVM swaps: token to sell, as a symbol like USDC/ETH or a token contract address.' },
1385
+ buyToken: { type: 'string', description: 'For EVM swaps: token to buy, as a symbol like ETH/WETH/USDC or a token contract address.' },
1386
+ sellAmount: { type: 'string', description: 'For EVM swaps: amount to sell in display units.' },
1387
+ sellAmountAtomic: { type: 'string', description: 'For EVM swaps: amount to sell in atomic units.' },
1388
+ recipient: { type: 'string', description: 'Optional swap recipient. Defaults to the wallet address.' },
1389
+ slippageBps: { type: 'string', description: 'Optional max slippage in basis points. Also accepts simple percentage-like inputs such as 1 for 1%.' },
1390
+ data: { type: 'string' },
1391
+ valueAtomic: { type: 'string', description: 'Native value in atomic units such as wei or lamports.' },
1392
+ typedData: { type: 'string', description: 'Typed-data JSON object.' },
1393
+ domain: { type: 'string', description: 'Typed-data domain JSON object.' },
1394
+ types: { type: 'string', description: 'Typed-data types JSON object.' },
1395
+ value: { type: 'string', description: 'Typed-data value JSON object.' },
1396
+ transaction: { type: 'string', description: 'Transaction JSON object.' },
1397
+ transactionBase64: { type: 'string' },
1398
+ signedTransaction: { type: 'string' },
1399
+ signedTransactionBase64: { type: 'string' },
1400
+ waitForReceipt: { type: 'boolean' },
1401
+ waitForConfirmation: { type: 'boolean' },
1402
+ approvalId: { type: 'string', description: 'The approval request id that was manually approved by the user for this exact wallet action.' },
1403
+ approved: { type: 'boolean', description: 'Set to true only after the user has manually approved the requested wallet action.' },
198
1404
  },
199
- required: ['action']
1405
+ required: ['action'],
200
1406
  },
201
- execute: async (args, context) => executeWalletAction(args, { agentId: context.session.agentId })
202
- }
203
- ]
1407
+ execute: async (args, context) => executeWalletAction(args, { agentId: context.session.agentId, sessionId: context.session.id }),
1408
+ },
1409
+ ],
204
1410
  }
205
1411
 
206
1412
  getPluginManager().registerBuiltin('wallet', WalletPlugin)
207
1413
 
208
- /**
209
- * Legacy Bridge
210
- */
211
1414
  export function buildWalletTools(bctx: ToolBuildContext): StructuredToolInterface[] {
212
1415
  if (!bctx.hasPlugin('wallet')) return []
213
1416
  return [
214
1417
  tool(
215
- async (args) => executeWalletAction(args, { agentId: bctx.ctx?.agentId }),
1418
+ async (args) => executeWalletAction(args, { agentId: bctx.ctx?.agentId, sessionId: bctx.ctx?.sessionId }),
216
1419
  {
217
1420
  name: 'wallet_tool',
218
1421
  description: WalletPlugin.tools![0].description,
219
1422
  schema: z.object({
220
- action: z.enum(['balance', 'address', 'send', 'transactions']),
1423
+ action: z.enum(WALLET_TOOL_ACTIONS),
1424
+ chain: z.enum(['solana', 'ethereum']).optional().describe('Choose a specific wallet chain when the agent has multiple wallets.'),
1425
+ provider: z.string().optional(),
1426
+ network: z.string().optional(),
1427
+ rpcUrl: z.string().optional(),
1428
+ label: z.string().optional(),
221
1429
  toAddress: z.string().optional(),
1430
+ contractAddress: z.string().optional(),
1431
+ amount: z.string().optional(),
222
1432
  amountSol: z.number().optional(),
223
1433
  memo: z.string().optional(),
224
1434
  limit: z.number().optional(),
225
- approved: z.boolean().optional()
226
- })
227
- }
228
- )
1435
+ message: z.string().optional(),
1436
+ messageHex: z.string().optional(),
1437
+ messageBase64: z.string().optional(),
1438
+ abi: z.any().optional(),
1439
+ functionName: z.string().optional(),
1440
+ args: z.any().optional(),
1441
+ sellToken: z.string().optional(),
1442
+ buyToken: z.string().optional(),
1443
+ sellAmount: z.string().optional(),
1444
+ sellAmountAtomic: z.union([z.string(), z.number()]).optional(),
1445
+ recipient: z.string().optional(),
1446
+ slippageBps: z.union([z.string(), z.number()]).optional(),
1447
+ data: z.string().optional(),
1448
+ valueAtomic: z.union([z.string(), z.number()]).optional(),
1449
+ typedData: z.any().optional(),
1450
+ domain: z.any().optional(),
1451
+ types: z.any().optional(),
1452
+ value: z.any().optional(),
1453
+ transaction: z.any().optional(),
1454
+ transactionBase64: z.string().optional(),
1455
+ signedTransaction: z.string().optional(),
1456
+ signedTransactionBase64: z.string().optional(),
1457
+ waitForReceipt: z.boolean().optional(),
1458
+ waitForConfirmation: z.boolean().optional(),
1459
+ approvalId: z.string().optional(),
1460
+ approved: z.boolean().optional(),
1461
+ }),
1462
+ },
1463
+ ),
229
1464
  ]
230
1465
  }