@swarmclawai/swarmclaw 1.2.8 → 1.3.0

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