@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
@@ -0,0 +1,590 @@
1
+ import {
2
+ JsonRpcProvider,
3
+ Interface,
4
+ ParamType,
5
+ Result,
6
+ Wallet,
7
+ getBytes,
8
+ getAddress,
9
+ isAddress,
10
+ keccak256,
11
+ type JsonFragment,
12
+ type TransactionRequest,
13
+ type TypedDataDomain,
14
+ type TypedDataField,
15
+ } from 'ethers'
16
+
17
+ import { decryptKey, encryptKey } from './storage'
18
+
19
+ export type EvmNetworkId = 'ethereum' | 'arbitrum' | 'base'
20
+
21
+ export interface EvmNetworkConfig {
22
+ id: EvmNetworkId
23
+ label: string
24
+ chainId: number
25
+ rpcUrl: string
26
+ addressExplorerBaseUrl: string
27
+ transactionExplorerBaseUrl: string
28
+ }
29
+
30
+ export interface EthereumExecutionOptions {
31
+ network?: EvmNetworkId | string | null
32
+ rpcUrl?: string | null
33
+ }
34
+
35
+ export interface EthereumMessageInput {
36
+ message?: string | null
37
+ messageHex?: string | null
38
+ messageBase64?: string | null
39
+ }
40
+
41
+ function serializeEvmValue(value: unknown): unknown {
42
+ if (typeof value === 'bigint') return value.toString()
43
+ if (Array.isArray(value)) return value.map((entry) => serializeEvmValue(entry))
44
+ if (value instanceof Uint8Array) return `0x${Buffer.from(value).toString('hex')}`
45
+ if (value && typeof value === 'object') {
46
+ if (value instanceof Result) {
47
+ return Array.from(value).map((entry) => serializeEvmValue(entry))
48
+ }
49
+ const out: Record<string, unknown> = {}
50
+ for (const [key, entry] of Object.entries(value)) {
51
+ if (/^\d+$/.test(key)) continue
52
+ out[key] = serializeEvmValue(entry)
53
+ }
54
+ return out
55
+ }
56
+ return value
57
+ }
58
+
59
+ const DEFAULT_RPC_URL = process.env.ETHEREUM_RPC_URL || process.env.EVM_RPC_URL || 'https://ethereum-rpc.publicnode.com'
60
+ const DEFAULT_EVM_RPC_TIMEOUT_MS = (() => {
61
+ const parsed = Number.parseInt(process.env.EVM_RPC_TIMEOUT_MS || '20000', 10)
62
+ if (!Number.isFinite(parsed) || parsed <= 0) return 20_000
63
+ return parsed
64
+ })()
65
+
66
+ const EVM_NETWORKS: Record<EvmNetworkId, EvmNetworkConfig> = {
67
+ ethereum: {
68
+ id: 'ethereum',
69
+ label: 'Ethereum',
70
+ chainId: 1,
71
+ rpcUrl: process.env.ETHEREUM_RPC_URL || process.env.EVM_RPC_URL || 'https://ethereum-rpc.publicnode.com',
72
+ addressExplorerBaseUrl: 'https://etherscan.io/address/',
73
+ transactionExplorerBaseUrl: 'https://etherscan.io/tx/',
74
+ },
75
+ arbitrum: {
76
+ id: 'arbitrum',
77
+ label: 'Arbitrum',
78
+ chainId: 42161,
79
+ rpcUrl: process.env.ARBITRUM_RPC_URL || 'https://arbitrum-one-rpc.publicnode.com',
80
+ addressExplorerBaseUrl: 'https://arbiscan.io/address/',
81
+ transactionExplorerBaseUrl: 'https://arbiscan.io/tx/',
82
+ },
83
+ base: {
84
+ id: 'base',
85
+ label: 'Base',
86
+ chainId: 8453,
87
+ rpcUrl: process.env.BASE_RPC_URL || 'https://base-rpc.publicnode.com',
88
+ addressExplorerBaseUrl: 'https://basescan.org/address/',
89
+ transactionExplorerBaseUrl: 'https://basescan.org/tx/',
90
+ },
91
+ }
92
+
93
+ function normalizeHexData(value: string, fieldName: string): string {
94
+ const trimmed = value.trim()
95
+ if (!/^0x[0-9a-fA-F]*$/.test(trimmed)) {
96
+ throw new Error(`${fieldName} must be a 0x-prefixed hex string`)
97
+ }
98
+ return trimmed
99
+ }
100
+
101
+ function parseBigIntField(value: unknown, fieldName: string): bigint | undefined {
102
+ if (value === undefined || value === null || value === '') return undefined
103
+ if (typeof value === 'bigint') return value
104
+ if (typeof value === 'number' && Number.isFinite(value)) return BigInt(Math.trunc(value))
105
+ if (typeof value === 'string') {
106
+ const trimmed = value.trim()
107
+ if (!trimmed) return undefined
108
+ if (/^\d+$/.test(trimmed)) return BigInt(trimmed)
109
+ if (/^0x[0-9a-fA-F]+$/.test(trimmed)) return BigInt(trimmed)
110
+ }
111
+ throw new Error(`${fieldName} must be an integer or hex quantity`)
112
+ }
113
+
114
+ function parseNumberField(value: unknown, fieldName: string): number | undefined {
115
+ if (value === undefined || value === null || value === '') return undefined
116
+ if (typeof value === 'number' && Number.isFinite(value)) return Math.trunc(value)
117
+ if (typeof value === 'bigint') return Number(value)
118
+ if (typeof value === 'string') {
119
+ const trimmed = value.trim()
120
+ if (!trimmed) return undefined
121
+ if (/^\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10)
122
+ if (/^0x[0-9a-fA-F]+$/.test(trimmed)) return Number(BigInt(trimmed))
123
+ }
124
+ throw new Error(`${fieldName} must be an integer`)
125
+ }
126
+
127
+ function normalizeAddressInput(value: string, fieldName: string): string {
128
+ const trimmed = value.trim()
129
+ if (!/^0x[0-9a-fA-F]{40}$/.test(trimmed)) {
130
+ throw new Error(`${fieldName} must be a 20-byte hex address`)
131
+ }
132
+ return getAddress(trimmed.toLowerCase())
133
+ }
134
+
135
+ function normalizeAbiArgument(param: ParamType, value: unknown, fieldName: string): unknown {
136
+ if (param.baseType === 'address') {
137
+ if (typeof value !== 'string') throw new Error(`${fieldName} must be an address string`)
138
+ return normalizeAddressInput(value, fieldName)
139
+ }
140
+ if (param.baseType === 'array') {
141
+ if (!Array.isArray(value)) throw new Error(`${fieldName} must be an array`)
142
+ return value.map((entry, index) => normalizeAbiArgument(param.arrayChildren!, entry, `${fieldName}[${index}]`))
143
+ }
144
+ if (param.baseType === 'tuple') {
145
+ const components = param.components ?? []
146
+ if (Array.isArray(value)) {
147
+ return components.map((component, index) => normalizeAbiArgument(component, value[index], `${fieldName}[${index}]`))
148
+ }
149
+ if (!value || typeof value !== 'object') throw new Error(`${fieldName} must be an object or array for tuple input`)
150
+ return components.map((component, index) => {
151
+ const record = value as Record<string, unknown>
152
+ const componentValue = component.name && component.name in record
153
+ ? record[component.name]
154
+ : record[String(index)]
155
+ return normalizeAbiArgument(component, componentValue, `${fieldName}.${component.name || index}`)
156
+ })
157
+ }
158
+ return value
159
+ }
160
+
161
+ function normalizeFunctionArgs(
162
+ fragment: NonNullable<ReturnType<Interface['getFunction']>>,
163
+ args: unknown[] | Record<string, unknown>,
164
+ functionName: string,
165
+ ): unknown[] {
166
+ const source = Array.isArray(args) ? args : args && typeof args === 'object' ? args : []
167
+ if (!Array.isArray(source) && fragment.inputs.length === 1 && fragment.inputs[0].baseType === 'tuple') {
168
+ const tupleInput = fragment.inputs[0]
169
+ const hasNamedWrapper = tupleInput.name && tupleInput.name in source
170
+ const hasIndexWrapper = '0' in source
171
+ if (!hasNamedWrapper && !hasIndexWrapper) {
172
+ return [normalizeAbiArgument(tupleInput, source, `${functionName}.args[${tupleInput.name || 0}]`)]
173
+ }
174
+ }
175
+ return fragment.inputs.map((input, index) => {
176
+ const rawValue = Array.isArray(source)
177
+ ? source[index]
178
+ : input.name && input.name in source
179
+ ? source[input.name]
180
+ : source[String(index)]
181
+ return normalizeAbiArgument(input, rawValue, `${functionName}.args[${input.name || index}]`)
182
+ })
183
+ }
184
+
185
+ function normalizeMessageInput(input: EthereumMessageInput): string | Uint8Array {
186
+ if (typeof input.messageHex === 'string' && input.messageHex.trim()) {
187
+ return getBytes(normalizeHexData(input.messageHex, 'messageHex'))
188
+ }
189
+ if (typeof input.messageBase64 === 'string' && input.messageBase64.trim()) {
190
+ return Uint8Array.from(Buffer.from(input.messageBase64.trim(), 'base64'))
191
+ }
192
+ if (typeof input.message === 'string') return input.message
193
+ throw new Error('message, messageHex, or messageBase64 is required')
194
+ }
195
+
196
+ function normalizeTypedDataDomain(domain: Record<string, unknown>): TypedDataDomain {
197
+ const normalized: TypedDataDomain = { ...domain }
198
+ if (domain.chainId !== undefined) {
199
+ normalized.chainId = parseBigIntField(domain.chainId, 'typed data domain.chainId')
200
+ }
201
+ return normalized
202
+ }
203
+
204
+ function normalizeTypedDataTypes(types: Record<string, unknown>): Record<string, TypedDataField[]> {
205
+ const out: Record<string, TypedDataField[]> = {}
206
+ for (const [key, value] of Object.entries(types)) {
207
+ if (key === 'EIP712Domain') continue
208
+ if (!Array.isArray(value)) throw new Error(`typed data types.${key} must be an array`)
209
+ out[key] = value.map((entry) => {
210
+ if (!entry || typeof entry !== 'object') throw new Error(`typed data types.${key} entries must be objects`)
211
+ const field = entry as Record<string, unknown>
212
+ if (typeof field.name !== 'string' || typeof field.type !== 'string') {
213
+ throw new Error(`typed data types.${key} entries require name and type`)
214
+ }
215
+ return { name: field.name, type: field.type }
216
+ })
217
+ }
218
+ return out
219
+ }
220
+
221
+ function normalizeAbiInput(abi: unknown): ReadonlyArray<string | JsonFragment> {
222
+ if (Array.isArray(abi)) return abi as ReadonlyArray<string | JsonFragment>
223
+ if (typeof abi === 'string') {
224
+ const trimmed = abi.trim()
225
+ if (!trimmed) throw new Error('abi is required')
226
+ if (trimmed.startsWith('[')) {
227
+ const parsed = JSON.parse(trimmed)
228
+ if (!Array.isArray(parsed)) throw new Error('abi JSON must be an array')
229
+ return parsed as ReadonlyArray<string | JsonFragment>
230
+ }
231
+ return [trimmed]
232
+ }
233
+ throw new Error('abi must be an array or JSON string')
234
+ }
235
+
236
+ function normalizeTransactionRequest(tx: Record<string, unknown>): TransactionRequest {
237
+ const normalized: TransactionRequest = {}
238
+ if (tx.to !== undefined && tx.to !== null && tx.to !== '') normalized.to = normalizeAddressInput(String(tx.to), 'transaction.to')
239
+ if (tx.data !== undefined && tx.data !== null && tx.data !== '') normalized.data = normalizeHexData(String(tx.data), 'transaction.data')
240
+ if (tx.value !== undefined) normalized.value = parseBigIntField(tx.value, 'transaction.value')
241
+ if (tx.nonce !== undefined) normalized.nonce = parseNumberField(tx.nonce, 'transaction.nonce')
242
+ if (tx.chainId !== undefined) normalized.chainId = parseNumberField(tx.chainId, 'transaction.chainId')
243
+ if (tx.type !== undefined) normalized.type = parseNumberField(tx.type, 'transaction.type')
244
+ if (tx.gasLimit !== undefined) normalized.gasLimit = parseBigIntField(tx.gasLimit, 'transaction.gasLimit')
245
+ if (tx.gasPrice !== undefined) normalized.gasPrice = parseBigIntField(tx.gasPrice, 'transaction.gasPrice')
246
+ if (tx.maxFeePerGas !== undefined) normalized.maxFeePerGas = parseBigIntField(tx.maxFeePerGas, 'transaction.maxFeePerGas')
247
+ if (tx.maxPriorityFeePerGas !== undefined) normalized.maxPriorityFeePerGas = parseBigIntField(tx.maxPriorityFeePerGas, 'transaction.maxPriorityFeePerGas')
248
+ if (tx.accessList !== undefined) normalized.accessList = tx.accessList as TransactionRequest['accessList']
249
+ return normalized
250
+ }
251
+
252
+ async function withEthereumRpcTimeout<T>(promise: Promise<T>, label: string, timeoutMs = DEFAULT_EVM_RPC_TIMEOUT_MS): Promise<T> {
253
+ let timer: ReturnType<typeof setTimeout> | null = null
254
+ try {
255
+ return await Promise.race([
256
+ promise,
257
+ new Promise<never>((_, reject) => {
258
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
259
+ }),
260
+ ])
261
+ } finally {
262
+ if (timer) clearTimeout(timer)
263
+ }
264
+ }
265
+
266
+ async function resolveWalletAndTransaction(
267
+ encryptedPrivateKey: string,
268
+ tx: Record<string, unknown>,
269
+ options?: EthereumExecutionOptions,
270
+ ): Promise<{ provider: JsonRpcProvider; wallet: Wallet; txRequest: TransactionRequest; network: EvmNetworkConfig }> {
271
+ const network = getEvmNetworkConfig(options?.network)
272
+ const provider = getProviderForNetwork(options?.network, options?.rpcUrl)
273
+ const wallet = getWalletFromEncrypted(encryptedPrivateKey).connect(provider)
274
+ const fromAddress = typeof tx.from === 'string' ? tx.from.trim() : ''
275
+ if (fromAddress && fromAddress.toLowerCase() !== wallet.address.toLowerCase()) {
276
+ throw new Error(`transaction.from does not match wallet address ${wallet.address}`)
277
+ }
278
+ const txRequest = normalizeTransactionRequest(tx)
279
+ if (txRequest.chainId == null) txRequest.chainId = network.chainId
280
+ const populated = await withEthereumRpcTimeout(
281
+ wallet.populateTransaction(txRequest),
282
+ `populate transaction on ${network.label}`,
283
+ )
284
+ return { provider, wallet, txRequest: populated, network }
285
+ }
286
+
287
+ export function generateEthereumWallet(): { publicKey: string; encryptedPrivateKey: string } {
288
+ const wallet = Wallet.createRandom()
289
+ return {
290
+ publicKey: wallet.address,
291
+ encryptedPrivateKey: encryptKey(wallet.privateKey),
292
+ }
293
+ }
294
+
295
+ export function getWalletFromEncrypted(encryptedPrivateKey: string): Wallet {
296
+ return new Wallet(decryptKey(encryptedPrivateKey))
297
+ }
298
+
299
+ export function normalizeEvmNetwork(value: unknown, fallback: EvmNetworkId = 'ethereum'): EvmNetworkId {
300
+ const normalized = String(value ?? '').trim().toLowerCase()
301
+ if (!normalized) return fallback
302
+ if (normalized === 'ethereum' || normalized === 'eth' || normalized === 'mainnet') return 'ethereum'
303
+ if (normalized === 'arbitrum' || normalized === 'arb' || normalized === 'arbitrum-one') return 'arbitrum'
304
+ if (normalized === 'base') return 'base'
305
+ throw new Error(`Unsupported EVM network: ${String(value)}`)
306
+ }
307
+
308
+ export function getEvmNetworkConfig(value?: unknown): EvmNetworkConfig {
309
+ return EVM_NETWORKS[normalizeEvmNetwork(value)]
310
+ }
311
+
312
+ export function listEvmNetworkConfigs(): EvmNetworkConfig[] {
313
+ return Object.values(EVM_NETWORKS)
314
+ }
315
+
316
+ export function getEvmExplorerUrl(network: EvmNetworkId | string | null | undefined, kind: 'address' | 'transaction', value: string): string {
317
+ const config = getEvmNetworkConfig(network)
318
+ return `${kind === 'address' ? config.addressExplorerBaseUrl : config.transactionExplorerBaseUrl}${value}`
319
+ }
320
+
321
+ export function getProvider(rpcUrl?: string): JsonRpcProvider {
322
+ return new JsonRpcProvider(rpcUrl || DEFAULT_RPC_URL)
323
+ }
324
+
325
+ export function getProviderForNetwork(network?: EvmNetworkId | string | null, rpcUrl?: string | null): JsonRpcProvider {
326
+ return new JsonRpcProvider(rpcUrl || getEvmNetworkConfig(network).rpcUrl)
327
+ }
328
+
329
+ export async function getBalance(address: string, rpcUrl?: string): Promise<bigint> {
330
+ return withEthereumRpcTimeout(getProvider(rpcUrl).getBalance(address), 'get balance')
331
+ }
332
+
333
+ export async function sendEth(
334
+ encryptedPrivateKey: string,
335
+ toAddress: string,
336
+ amountWei: string,
337
+ rpcUrl?: string,
338
+ ): Promise<{ signature: string; fee?: string }> {
339
+ const provider = getProvider(rpcUrl)
340
+ const wallet = getWalletFromEncrypted(encryptedPrivateKey).connect(provider)
341
+ const tx = await withEthereumRpcTimeout(wallet.sendTransaction({
342
+ to: toAddress,
343
+ value: BigInt(amountWei),
344
+ }), 'send ETH transaction')
345
+ const receipt = await withEthereumRpcTimeout(tx.wait(), 'wait for ETH transaction receipt')
346
+ return {
347
+ signature: tx.hash,
348
+ fee: receipt?.fee ? receipt.fee.toString() : undefined,
349
+ }
350
+ }
351
+
352
+ export function encodeEthereumContractCall(
353
+ abi: unknown,
354
+ functionName: string,
355
+ args: unknown[] | Record<string, unknown> = [],
356
+ ): { data: string; fragment: string } {
357
+ const iface = new Interface(normalizeAbiInput(abi))
358
+ const fragment = iface.getFunction(functionName)
359
+ if (!fragment) throw new Error(`Function not found in ABI: ${functionName}`)
360
+ const normalizedArgs = normalizeFunctionArgs(fragment, args, functionName)
361
+ return {
362
+ data: iface.encodeFunctionData(fragment, normalizedArgs),
363
+ fragment: fragment.format('full'),
364
+ }
365
+ }
366
+
367
+ export async function callEthereumContract(
368
+ encryptedPrivateKey: string,
369
+ input: {
370
+ contractAddress: string
371
+ abi: unknown
372
+ functionName: string
373
+ args?: unknown[] | Record<string, unknown>
374
+ },
375
+ options?: EthereumExecutionOptions,
376
+ ): Promise<{
377
+ network: EvmNetworkConfig
378
+ address: string
379
+ fragment: string
380
+ data: string
381
+ rawResult: string
382
+ decoded: unknown
383
+ namedOutputs: Record<string, unknown>
384
+ }> {
385
+ const network = getEvmNetworkConfig(options?.network)
386
+ const provider = getProviderForNetwork(options?.network, options?.rpcUrl)
387
+ const wallet = getWalletFromEncrypted(encryptedPrivateKey)
388
+ const iface = new Interface(normalizeAbiInput(input.abi))
389
+ const fragment = iface.getFunction(input.functionName)
390
+ if (!fragment) throw new Error(`Function not found in ABI: ${input.functionName}`)
391
+ const normalizedArgs = normalizeFunctionArgs(fragment, input.args || [], input.functionName)
392
+ const data = iface.encodeFunctionData(fragment, normalizedArgs)
393
+ const rawResult = await withEthereumRpcTimeout(provider.call({
394
+ to: normalizeAddressInput(input.contractAddress, 'contractAddress'),
395
+ data,
396
+ from: wallet.address,
397
+ }), `call contract ${input.functionName} on ${network.label}`)
398
+ const decodedResult = iface.decodeFunctionResult(fragment, rawResult)
399
+ const decodedValues = Array.from(decodedResult).map((entry) => serializeEvmValue(entry))
400
+ const namedOutputs: Record<string, unknown> = {}
401
+ for (let index = 0; index < fragment.outputs?.length; index += 1) {
402
+ const output = fragment.outputs[index]
403
+ if (!output?.name) continue
404
+ namedOutputs[output.name] = serializeEvmValue(decodedResult[index])
405
+ }
406
+
407
+ return {
408
+ network,
409
+ address: wallet.address,
410
+ fragment: fragment.format('full'),
411
+ data,
412
+ rawResult,
413
+ decoded: decodedValues.length === 1 ? decodedValues[0] : decodedValues,
414
+ namedOutputs,
415
+ }
416
+ }
417
+
418
+ export async function signEthereumMessage(
419
+ encryptedPrivateKey: string,
420
+ input: EthereumMessageInput,
421
+ ): Promise<{ signature: string; address: string }> {
422
+ const wallet = getWalletFromEncrypted(encryptedPrivateKey)
423
+ return {
424
+ signature: await wallet.signMessage(normalizeMessageInput(input)),
425
+ address: wallet.address,
426
+ }
427
+ }
428
+
429
+ export async function signEthereumTypedData(
430
+ encryptedPrivateKey: string,
431
+ input: {
432
+ domain: Record<string, unknown>
433
+ types: Record<string, unknown>
434
+ value: Record<string, unknown>
435
+ },
436
+ ): Promise<{ signature: string; address: string }> {
437
+ const wallet = getWalletFromEncrypted(encryptedPrivateKey)
438
+ return {
439
+ signature: await wallet.signTypedData(
440
+ normalizeTypedDataDomain(input.domain),
441
+ normalizeTypedDataTypes(input.types),
442
+ input.value,
443
+ ),
444
+ address: wallet.address,
445
+ }
446
+ }
447
+
448
+ export async function signEthereumTransaction(
449
+ encryptedPrivateKey: string,
450
+ tx: Record<string, unknown>,
451
+ options?: EthereumExecutionOptions,
452
+ ): Promise<{
453
+ signedTransaction: string
454
+ transactionHash: string
455
+ address: string
456
+ chainId: number | null
457
+ network: EvmNetworkConfig
458
+ }> {
459
+ const { wallet, txRequest, network } = await resolveWalletAndTransaction(encryptedPrivateKey, tx, options)
460
+ const signedTransaction = await wallet.signTransaction(txRequest)
461
+ return {
462
+ signedTransaction,
463
+ transactionHash: keccak256(signedTransaction),
464
+ address: wallet.address,
465
+ chainId: txRequest.chainId != null ? Number(txRequest.chainId) : null,
466
+ network,
467
+ }
468
+ }
469
+
470
+ export async function simulateEthereumTransaction(
471
+ encryptedPrivateKey: string,
472
+ tx: Record<string, unknown>,
473
+ options?: EthereumExecutionOptions,
474
+ ): Promise<{
475
+ estimateGas?: string
476
+ callResult?: string
477
+ callError?: string
478
+ address: string
479
+ chainId: number | null
480
+ network: EvmNetworkConfig
481
+ }> {
482
+ const { provider, wallet, txRequest, network } = await resolveWalletAndTransaction(encryptedPrivateKey, tx, options)
483
+ let estimateGas: string | undefined
484
+ let callResult: string | undefined
485
+ let callError: string | undefined
486
+
487
+ try {
488
+ estimateGas = (await withEthereumRpcTimeout(
489
+ provider.estimateGas({ ...txRequest, from: wallet.address }),
490
+ `estimate gas on ${network.label}`,
491
+ )).toString()
492
+ } catch (err: unknown) {
493
+ callError = err instanceof Error ? err.message : String(err)
494
+ }
495
+
496
+ try {
497
+ callResult = await withEthereumRpcTimeout(
498
+ provider.call({ ...txRequest, from: wallet.address }),
499
+ `simulate transaction call on ${network.label}`,
500
+ )
501
+ } catch (err: unknown) {
502
+ if (!callError) callError = err instanceof Error ? err.message : String(err)
503
+ }
504
+
505
+ return {
506
+ estimateGas,
507
+ callResult,
508
+ callError,
509
+ address: wallet.address,
510
+ chainId: txRequest.chainId != null ? Number(txRequest.chainId) : null,
511
+ network,
512
+ }
513
+ }
514
+
515
+ export async function sendEthereumTransaction(
516
+ encryptedPrivateKey: string,
517
+ input: {
518
+ transaction?: Record<string, unknown>
519
+ signedTransaction?: string | null
520
+ waitForReceipt?: boolean
521
+ },
522
+ options?: EthereumExecutionOptions,
523
+ ): Promise<{
524
+ transactionHash: string
525
+ address: string
526
+ chainId: number | null
527
+ explorerUrl: string
528
+ receipt?: Record<string, unknown> | null
529
+ network: EvmNetworkConfig
530
+ }> {
531
+ const waitForReceipt = input.waitForReceipt === true
532
+ const network = getEvmNetworkConfig(options?.network)
533
+ const provider = getProviderForNetwork(options?.network, options?.rpcUrl)
534
+ const wallet = getWalletFromEncrypted(encryptedPrivateKey).connect(provider)
535
+
536
+ if (typeof input.signedTransaction === 'string' && input.signedTransaction.trim()) {
537
+ const response = await withEthereumRpcTimeout(
538
+ provider.broadcastTransaction(input.signedTransaction.trim()),
539
+ `broadcast signed transaction on ${network.label}`,
540
+ )
541
+ const receipt = waitForReceipt
542
+ ? await withEthereumRpcTimeout(response.wait(), `wait for transaction receipt on ${network.label}`)
543
+ : null
544
+ return {
545
+ transactionHash: response.hash,
546
+ address: wallet.address,
547
+ chainId: network.chainId,
548
+ explorerUrl: getEvmExplorerUrl(network.id, 'transaction', response.hash),
549
+ receipt: receipt ? {
550
+ blockHash: receipt.blockHash,
551
+ blockNumber: receipt.blockNumber,
552
+ gasUsed: receipt.gasUsed?.toString?.(),
553
+ fee: receipt.fee?.toString?.(),
554
+ status: receipt.status,
555
+ } : null,
556
+ network,
557
+ }
558
+ }
559
+
560
+ if (!input.transaction || typeof input.transaction !== 'object') {
561
+ throw new Error('transaction or signedTransaction is required')
562
+ }
563
+
564
+ const { txRequest } = await resolveWalletAndTransaction(encryptedPrivateKey, input.transaction, options)
565
+ const response = await withEthereumRpcTimeout(
566
+ wallet.sendTransaction(txRequest),
567
+ `send transaction on ${network.label}`,
568
+ )
569
+ const receipt = waitForReceipt
570
+ ? await withEthereumRpcTimeout(response.wait(), `wait for transaction receipt on ${network.label}`)
571
+ : null
572
+ return {
573
+ transactionHash: response.hash,
574
+ address: wallet.address,
575
+ chainId: txRequest.chainId != null ? Number(txRequest.chainId) : network.chainId,
576
+ explorerUrl: getEvmExplorerUrl(network.id, 'transaction', response.hash),
577
+ receipt: receipt ? {
578
+ blockHash: receipt.blockHash,
579
+ blockNumber: receipt.blockNumber,
580
+ gasUsed: receipt.gasUsed?.toString?.(),
581
+ fee: receipt.fee?.toString?.(),
582
+ status: receipt.status,
583
+ } : null,
584
+ network,
585
+ }
586
+ }
587
+
588
+ export function isValidEthereumAddress(address: string): boolean {
589
+ return isAddress(address)
590
+ }