@swarmclawai/swarmclaw 1.2.8 → 1.2.9

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