@0xsequence/catapult 1.4.0 → 1.5.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 (163) hide show
  1. package/README.md +27 -0
  2. package/dist/lib/__tests__/network-loader.spec.js.map +1 -1
  3. package/dist/lib/core/__tests__/resolver.spec.js +22 -0
  4. package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
  5. package/dist/lib/core/__tests__/sign-actions.spec.d.ts +2 -0
  6. package/dist/lib/core/__tests__/sign-actions.spec.d.ts.map +1 -0
  7. package/dist/lib/core/__tests__/sign-actions.spec.js +128 -0
  8. package/dist/lib/core/__tests__/sign-actions.spec.js.map +1 -0
  9. package/dist/lib/core/__tests__/signer.spec.d.ts +2 -0
  10. package/dist/lib/core/__tests__/signer.spec.d.ts.map +1 -0
  11. package/dist/lib/core/__tests__/signer.spec.js +40 -0
  12. package/dist/lib/core/__tests__/signer.spec.js.map +1 -0
  13. package/dist/lib/core/context.d.ts +3 -2
  14. package/dist/lib/core/context.d.ts.map +1 -1
  15. package/dist/lib/core/context.js +3 -2
  16. package/dist/lib/core/context.js.map +1 -1
  17. package/dist/lib/core/engine.d.ts +4 -0
  18. package/dist/lib/core/engine.d.ts.map +1 -1
  19. package/dist/lib/core/engine.js +173 -0
  20. package/dist/lib/core/engine.js.map +1 -1
  21. package/dist/lib/core/signer.d.ts +7 -0
  22. package/dist/lib/core/signer.d.ts.map +1 -0
  23. package/dist/lib/core/signer.js +60 -0
  24. package/dist/lib/core/signer.js.map +1 -0
  25. package/dist/lib/parsers/__tests__/source.spec.js +37 -0
  26. package/dist/lib/parsers/__tests__/source.spec.js.map +1 -1
  27. package/dist/lib/parsers/source.js +1 -1
  28. package/dist/lib/parsers/source.js.map +1 -1
  29. package/dist/lib/provenance.js +51 -2
  30. package/dist/lib/provenance.js.map +1 -1
  31. package/dist/lib/types/actions.d.ts +26 -2
  32. package/dist/lib/types/actions.d.ts.map +1 -1
  33. package/dist/lib/types/actions.js +3 -0
  34. package/dist/lib/types/actions.js.map +1 -1
  35. package/dist/lib/types/source.d.ts +2 -0
  36. package/dist/lib/types/source.d.ts.map +1 -1
  37. package/package.json +4 -1
  38. package/.eslintrc.json +0 -29
  39. package/.github/workflows/ci.yml +0 -181
  40. package/CONCEPT.md +0 -24
  41. package/contracts/checked-call.huff +0 -65
  42. package/eslint.config.js +0 -48
  43. package/examples/jobs/guards-v1.yaml +0 -17
  44. package/examples/jobs/sequence-seq-0001-patch.yaml +0 -59
  45. package/examples/jobs/sequence-v1.yaml +0 -59
  46. package/examples/templates/sequence-factory-v1.yaml +0 -56
  47. package/jest.config.js +0 -25
  48. package/src/cli.ts +0 -18
  49. package/src/commands/common.ts +0 -61
  50. package/src/commands/dry.ts +0 -209
  51. package/src/commands/etherscan.ts +0 -360
  52. package/src/commands/index.ts +0 -6
  53. package/src/commands/list.ts +0 -262
  54. package/src/commands/provenance.ts +0 -120
  55. package/src/commands/run.ts +0 -146
  56. package/src/commands/utils.ts +0 -215
  57. package/src/index.ts +0 -67
  58. package/src/lib/__tests__/deployer-events.spec.ts +0 -338
  59. package/src/lib/__tests__/deployer.spec.ts +0 -2269
  60. package/src/lib/__tests__/network-loader.spec.ts +0 -150
  61. package/src/lib/__tests__/network-selection.spec.ts +0 -41
  62. package/src/lib/__tests__/network-utils.spec.ts +0 -230
  63. package/src/lib/__tests__/provenance.spec.ts +0 -208
  64. package/src/lib/artifacts/__tests__/fixtures/contract1.json +0 -19
  65. package/src/lib/artifacts/__tests__/fixtures/contract2.json +0 -19
  66. package/src/lib/artifacts/__tests__/fixtures/duplicate-name.json +0 -19
  67. package/src/lib/artifacts/__tests__/fixtures/nested/nested-contract.json +0 -18
  68. package/src/lib/artifacts/__tests__/fixtures/not-an-artifact.json +0 -8
  69. package/src/lib/artifacts/__tests__/fixtures/readme.txt +0 -2
  70. package/src/lib/contracts/__tests__/repository.spec.ts +0 -612
  71. package/src/lib/contracts/repository.ts +0 -411
  72. package/src/lib/core/__tests__/assert-action.spec.ts +0 -474
  73. package/src/lib/core/__tests__/context.spec.ts +0 -37
  74. package/src/lib/core/__tests__/engine.spec.ts +0 -2005
  75. package/src/lib/core/__tests__/graph.spec.ts +0 -125
  76. package/src/lib/core/__tests__/json-integration.spec.ts +0 -425
  77. package/src/lib/core/__tests__/loader.spec.ts +0 -367
  78. package/src/lib/core/__tests__/multi-platform-verification.spec.ts +0 -406
  79. package/src/lib/core/__tests__/resolver.spec.ts +0 -2496
  80. package/src/lib/core/__tests__/static-action.spec.ts +0 -172
  81. package/src/lib/core/context.ts +0 -127
  82. package/src/lib/core/engine.ts +0 -1834
  83. package/src/lib/core/graph.ts +0 -252
  84. package/src/lib/core/loader.ts +0 -253
  85. package/src/lib/core/resolver.ts +0 -873
  86. package/src/lib/deployer.ts +0 -1005
  87. package/src/lib/events/__tests__/event-system.spec.ts +0 -392
  88. package/src/lib/events/cli-adapter.ts +0 -369
  89. package/src/lib/events/emitter.ts +0 -62
  90. package/src/lib/events/index.ts +0 -3
  91. package/src/lib/events/types.ts +0 -520
  92. package/src/lib/index.ts +0 -17
  93. package/src/lib/network-loader.ts +0 -90
  94. package/src/lib/network-selection.ts +0 -73
  95. package/src/lib/network-utils.ts +0 -64
  96. package/src/lib/parsers/__tests__/buildinfo.spec.ts +0 -122
  97. package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-bytecode-buildinfo.json +0 -62
  98. package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-json.txt +0 -2
  99. package/src/lib/parsers/__tests__/fixtures/buildinfo/multi-contract-buildinfo.json +0 -89
  100. package/src/lib/parsers/__tests__/fixtures/buildinfo/no-contracts-buildinfo.json +0 -17
  101. package/src/lib/parsers/__tests__/fixtures/buildinfo/simple-buildinfo.json +0 -63
  102. package/src/lib/parsers/__tests__/fixtures/buildinfo/wrong-format.json +0 -4
  103. package/src/lib/parsers/__tests__/job.spec.ts +0 -439
  104. package/src/lib/parsers/__tests__/source.spec.ts +0 -134
  105. package/src/lib/parsers/__tests__/template.spec.ts +0 -111
  106. package/src/lib/parsers/artifact/__tests__/artifact.spec.ts +0 -117
  107. package/src/lib/parsers/artifact/__tests__/fixtures/empty-bytecode.json +0 -5
  108. package/src/lib/parsers/artifact/__tests__/fixtures/hardhat-artifact.json +0 -67
  109. package/src/lib/parsers/artifact/__tests__/fixtures/invalid-bytecode.json +0 -5
  110. package/src/lib/parsers/artifact/__tests__/fixtures/invalid-json.txt +0 -11
  111. package/src/lib/parsers/artifact/__tests__/fixtures/minimal-artifact.json +0 -5
  112. package/src/lib/parsers/artifact/__tests__/fixtures/missing-abi.json +0 -4
  113. package/src/lib/parsers/artifact/__tests__/fixtures/missing-bytecode.json +0 -11
  114. package/src/lib/parsers/artifact/__tests__/fixtures/missing-contract-name.json +0 -11
  115. package/src/lib/parsers/artifact/__tests__/fixtures/simple-artifact.json +0 -40
  116. package/src/lib/parsers/artifact/__tests__/fixtures/wrong-types.json +0 -7
  117. package/src/lib/parsers/artifact/foundry-1.2.ts +0 -72
  118. package/src/lib/parsers/artifact/index.ts +0 -27
  119. package/src/lib/parsers/artifact/types.ts +0 -9
  120. package/src/lib/parsers/buildinfo.ts +0 -127
  121. package/src/lib/parsers/constants.ts +0 -56
  122. package/src/lib/parsers/index.ts +0 -6
  123. package/src/lib/parsers/job.ts +0 -160
  124. package/src/lib/parsers/source.ts +0 -129
  125. package/src/lib/parsers/template.ts +0 -135
  126. package/src/lib/provenance.ts +0 -785
  127. package/src/lib/std/templates/arachnid-deterministic-deployment-proxy.yaml +0 -68
  128. package/src/lib/std/templates/assured-deployment.yaml +0 -46
  129. package/src/lib/std/templates/era-evm-predeploy.yaml +0 -35
  130. package/src/lib/std/templates/erc-2470.yaml +0 -70
  131. package/src/lib/std/templates/min-balance.yaml +0 -35
  132. package/src/lib/std/templates/nano-universal-deployer.yaml +0 -61
  133. package/src/lib/std/templates/raw-erc-2470.yaml +0 -62
  134. package/src/lib/std/templates/raw-nano-universal-deployer.yaml +0 -54
  135. package/src/lib/std/templates/raw-sequence-universal-deployer-2.yaml +0 -52
  136. package/src/lib/std/templates/sequence-universal-deployer-2.yaml +0 -61
  137. package/src/lib/types/__tests__/json-request-action.spec.ts +0 -243
  138. package/src/lib/types/__tests__/read-json-value.spec.ts +0 -278
  139. package/src/lib/types/__tests__/resolve-json-value.spec.ts +0 -769
  140. package/src/lib/types/actions.ts +0 -148
  141. package/src/lib/types/artifacts.ts +0 -21
  142. package/src/lib/types/buildinfo.ts +0 -116
  143. package/src/lib/types/conditions.ts +0 -50
  144. package/src/lib/types/contracts.ts +0 -26
  145. package/src/lib/types/definitions.ts +0 -77
  146. package/src/lib/types/index.ts +0 -9
  147. package/src/lib/types/network.ts +0 -33
  148. package/src/lib/types/project.ts +0 -9
  149. package/src/lib/types/source.ts +0 -26
  150. package/src/lib/types/task.ts +0 -9
  151. package/src/lib/types/values.ts +0 -221
  152. package/src/lib/utils/assertion.ts +0 -24
  153. package/src/lib/utils/validation.ts +0 -116
  154. package/src/lib/validation/contract-references.ts +0 -210
  155. package/src/lib/validation/index.ts +0 -1
  156. package/src/lib/verification/__tests__/etherscan.spec.ts +0 -710
  157. package/src/lib/verification/__tests__/sourcify.spec.ts +0 -288
  158. package/src/lib/verification/etherscan.ts +0 -547
  159. package/src/lib/verification/sourcify.ts +0 -248
  160. package/test_validation/artifacts/TestContract.json +0 -9
  161. package/test_validation/jobs/test-missing.yaml +0 -16
  162. package/test_validation/networks.yaml +0 -3
  163. package/tsconfig.json +0 -36
@@ -1,1834 +0,0 @@
1
- import { Job, Template, Action, JobAction, isPrimitiveActionType, Condition } from '../types'
2
- import { Contract } from '../types/contracts'
3
- import { ExecutionContext } from './context'
4
- import { ValueResolver, ResolutionScope } from './resolver'
5
- import { validateAddress, validateHexData, validateBigNumberish, validateRawTransaction } from '../utils/validation'
6
- import { DeploymentEventEmitter, deploymentEvents } from '../events'
7
- import { createDefaultVerificationRegistry, VerificationPlatformRegistry } from '../verification/etherscan'
8
- import { BuildInfo } from '../types/buildinfo'
9
- import { ethers } from 'ethers'
10
-
11
- export type EngineOptions = {
12
- eventEmitter?: DeploymentEventEmitter
13
- verificationRegistry?: VerificationPlatformRegistry
14
- noPostCheckConditions?: boolean
15
- allowMultipleNicksMethodTests?: boolean
16
- ignoreVerifyErrors?: boolean
17
- }
18
-
19
- /**
20
- * The ExecutionEngine is the core component that runs jobs and their actions.
21
- * It interprets the declarative YAML files, resolves values, interacts with the
22
- * blockchain, and manages the overall execution flow.
23
- */
24
- export class ExecutionEngine {
25
- private readonly resolver: ValueResolver
26
- private readonly templates: Map<string, Template>
27
- private readonly events: DeploymentEventEmitter
28
- private readonly verificationRegistry: VerificationPlatformRegistry
29
- private readonly noPostCheckConditions: boolean
30
- private readonly allowMultipleNicksMethodTests: boolean
31
- private readonly ignoreVerifyErrors: boolean
32
- private nicksMethodResult: boolean | undefined
33
- private verificationWarnings: Array<{
34
- actionName: string
35
- address: string
36
- contractName: string
37
- platform: string
38
- error: string
39
- jobName?: string
40
- networkName?: string
41
- }> = []
42
-
43
- constructor(templates: Map<string, Template>, options?: EngineOptions) {
44
- this.resolver = new ValueResolver()
45
- this.templates = templates
46
- this.events = options?.eventEmitter || deploymentEvents
47
- this.verificationRegistry = options?.verificationRegistry || createDefaultVerificationRegistry()
48
- this.noPostCheckConditions = options?.noPostCheckConditions ?? false
49
- this.allowMultipleNicksMethodTests = options?.allowMultipleNicksMethodTests ?? false
50
- this.ignoreVerifyErrors = options?.ignoreVerifyErrors ?? false
51
- }
52
-
53
- /**
54
- * Computes retry configuration for post-execution checks, tuned for local vs public networks.
55
- * Local (anvil/hardhat): 50ms delay for ~5s total (100 retries => 101 attempts)
56
- * Public: 2000ms delay for ~30s total (15 retries => 16 attempts)
57
- */
58
- private getPostCheckRetryConfig(context: ExecutionContext): { retries: number; delayMs: number } {
59
- const network = context.getNetwork()
60
- const isLocal =
61
- network.chainId === 31337 ||
62
- network.chainId === 1337 ||
63
- /localhost|127\.0\.0\.1/i.test(network.rpcUrl)
64
-
65
- if (isLocal) {
66
- return { retries: 100, delayMs: 50 }
67
- }
68
- return { retries: 15, delayMs: 2000 }
69
- }
70
-
71
- /**
72
- * Executes a single job against a given network context.
73
- * @param job The Job object to execute.
74
- * @param context The ExecutionContext for the target network.
75
- */
76
- public async executeJob(job: Job, context: ExecutionContext): Promise<void> {
77
- this.events.emitEvent({
78
- type: 'job_started',
79
- level: 'info',
80
- data: {
81
- jobName: job.name,
82
- jobVersion: job.version,
83
- networkName: context.getNetwork().name,
84
- chainId: context.getNetwork().chainId
85
- }
86
- })
87
-
88
- // Set context path for relative artifact resolution
89
- const previousContextPath = context.getContextPath()
90
- context.setContextPath(job._path)
91
-
92
- try {
93
- const executionOrder = this.topologicalSortActions(job)
94
-
95
- for (const actionName of executionOrder) {
96
- const action = job.actions.find(a => a.name === actionName)
97
- if (!action) {
98
- // This should be unreachable if topological sort is correct
99
- throw new Error(`Internal error: Action "${actionName}" not found in job "${job.name}".`)
100
- }
101
- await this.executeAction(action, context, new Map())
102
- }
103
-
104
- // If post-check conditions are enabled, re-evaluate job-level skip conditions with retry to handle RPC propagation lag
105
- if (!this.noPostCheckConditions && job.skip_condition) {
106
- const { retries, delayMs } = this.getPostCheckRetryConfig(context)
107
- const shouldSkip = await this.retryBooleanCheck(
108
- async () => this.evaluateSkipConditions(job.skip_condition!, context, new Map()),
109
- retries,
110
- delayMs
111
- )
112
- if (!shouldSkip) {
113
- // If skip conditions don't evaluate to true after execution, the job failed
114
- throw new Error(`Job "${job.name}" failed post-execution check: skip conditions did not evaluate to true`)
115
- }
116
- }
117
- } finally {
118
- // Restore previous context path
119
- context.setContextPath(previousContextPath)
120
- }
121
-
122
- this.events.emitEvent({
123
- type: 'job_completed',
124
- level: 'info',
125
- data: {
126
- jobName: job.name,
127
- networkName: context.getNetwork().name,
128
- chainId: context.getNetwork().chainId
129
- }
130
- })
131
- }
132
-
133
- /**
134
- * The central dispatcher for executing any action, whether it's a primitive
135
- * or a call to another template.
136
- * @param action The action to execute.
137
- * @param context The global execution context.
138
- * @param scope The local resolution scope, used for template arguments.
139
- */
140
- private async executeAction(
141
- action: JobAction | Action,
142
- context: ExecutionContext,
143
- scope: ResolutionScope,
144
- ): Promise<void> {
145
- const actionName = 'name' in action ? action.name : action.type
146
- // For JobAction, get template or type; for Action, get type
147
- const templateName = 'template' in action
148
- ? (action.template || action.type)
149
- : action.type
150
-
151
- if (!templateName) {
152
- throw new Error(`Action "${actionName}": missing both template and type fields`)
153
- }
154
-
155
- // Emit action start with a guaranteed, meaningful name
156
- const printableName =
157
- (typeof actionName === 'string' && actionName.trim().length > 0)
158
- ? actionName
159
- : (isPrimitiveActionType(templateName) ? templateName : `template:${templateName}`)
160
- this.events.emitEvent({
161
- type: 'action_started',
162
- level: 'info',
163
- data: {
164
- actionName: printableName,
165
- jobName: 'unknown' // We'll need to pass job context later
166
- }
167
- })
168
-
169
- // 1. Evaluate skip conditions for the action itself.
170
- const shouldSkip = await this.evaluateSkipConditions(action.skip_condition, context, scope)
171
- if (shouldSkip) {
172
- this.events.emitEvent({
173
- type: 'action_skipped',
174
- level: 'info',
175
- data: {
176
- actionName: actionName,
177
- reason: 'condition met'
178
- }
179
- })
180
-
181
- // Process static outputs even when action is skipped
182
- // This is important for static outputs that don't depend on action execution
183
- const hasCustomOutput = 'name' in action && action.name &&
184
- (action as any).output &&
185
- typeof (action as any).output === 'object' &&
186
- !Array.isArray((action as any).output)
187
-
188
- if (hasCustomOutput) {
189
- const customOutput = (action as any).output
190
- // Custom output map provided by job action: resolve each mapping within the current scope
191
- for (const [key, value] of Object.entries(customOutput)) {
192
- const resolvedOutput = await this.resolver.resolve(value as any, context, scope)
193
- const outputKey = `${action.name}.${key}`
194
- context.setOutput(outputKey, resolvedOutput)
195
- this.events.emitEvent({
196
- type: 'output_stored',
197
- level: 'debug',
198
- data: {
199
- outputKey,
200
- value: resolvedOutput
201
- }
202
- })
203
- }
204
- }
205
-
206
- return
207
- }
208
-
209
- // 2. Differentiate between a primitive action and a template call.
210
- if (isPrimitiveActionType(templateName)) {
211
- // Check if custom outputs are specified
212
- const hasCustomOutput = 'name' in action && action.name &&
213
- (action as any).output &&
214
- typeof (action as any).output === 'object' &&
215
- !Array.isArray((action as any).output)
216
-
217
- // Convert JobAction to Action for primitive execution
218
- const primitiveAction: Action = 'template' in action
219
- ? {
220
- type: (action.type || action.template) as any,
221
- name: action.name,
222
- arguments: action.arguments,
223
- skip_condition: action.skip_condition,
224
- depends_on: action.depends_on
225
- }
226
- : action as Action
227
-
228
- // Execute primitive with information about custom outputs
229
- await this.executePrimitive(primitiveAction, context, scope, hasCustomOutput)
230
-
231
- // Handle custom outputs for primitive actions (similar to template logic)
232
- if (hasCustomOutput) {
233
- const customOutput = (action as any).output
234
- // Custom output map provided by job action: resolve each mapping within the current scope
235
- for (const [key, value] of Object.entries(customOutput)) {
236
- const resolvedOutput = await this.resolver.resolve(value as any, context, scope)
237
- const outputKey = `${action.name}.${key}`
238
- context.setOutput(outputKey, resolvedOutput)
239
- this.events.emitEvent({
240
- type: 'output_stored',
241
- level: 'debug',
242
- data: {
243
- outputKey,
244
- value: resolvedOutput
245
- }
246
- })
247
- }
248
- }
249
- } else {
250
- await this.executeTemplate(action, templateName, context, scope)
251
- }
252
- }
253
-
254
- /**
255
- * Executes a template, including its setup, skip conditions, actions, and outputs.
256
- * @param callingAction The action from the parent job/template that is calling this template.
257
- * @param templateName The name of the template to execute.
258
- * @param context The global execution context.
259
- */
260
- private async executeTemplate(
261
- callingAction: JobAction | Action,
262
- templateName: string,
263
- context: ExecutionContext,
264
- parentScope: ResolutionScope = new Map(),
265
- ): Promise<void> {
266
- const template = this.templates.get(templateName)
267
- if (!template) {
268
- const actionName = 'name' in callingAction ? callingAction.name : callingAction.type
269
- throw new Error(`Template "${templateName}" not found for action "${actionName}".`)
270
- }
271
- this.events.emitEvent({
272
- type: 'template_entered',
273
- level: 'debug',
274
- data: {
275
- templateName: template.name
276
- }
277
- })
278
-
279
- // 1. Create and populate a new resolution scope for this template call.
280
- // NOTE: We resolve arguments in the CURRENT context (which should be the job's context)
281
- // before changing to the template's context. This ensures artifact references in
282
- // job arguments are resolved relative to the job, not the template.
283
- const templateScope: ResolutionScope = new Map()
284
- if ('arguments' in callingAction) {
285
- for (const [key, value] of Object.entries(callingAction.arguments)) {
286
- // Resolve the argument value in the parent's context, preserving the caller's local scope
287
- // so that template arguments from the caller are available when invoking nested templates.
288
- const resolvedValue = await this.resolver.resolve(value, context, parentScope)
289
- templateScope.set(key, resolvedValue)
290
- }
291
- }
292
-
293
- // Set context path for relative artifact resolution within the template
294
- const previousContextPath = context.getContextPath()
295
- context.setContextPath(template._path)
296
-
297
- try {
298
-
299
- // 2. Handle template-level setup block.
300
- if (template.setup) {
301
- // Check setup skip conditions before executing setup actions
302
- if (template.setup.skip_condition && await this.evaluateSkipConditions(template.setup.skip_condition, context, templateScope)) {
303
- this.events.emitEvent({
304
- type: 'template_setup_skipped',
305
- level: 'info',
306
- data: {
307
- templateName: template.name,
308
- reason: 'setup skip condition met'
309
- }
310
- })
311
- } else if (template.setup.actions) {
312
- this.events.emitEvent({
313
- type: 'template_setup_started',
314
- level: 'debug',
315
- data: {
316
- templateName: template.name
317
- }
318
- })
319
- for (const setupAction of template.setup.actions) {
320
- // Setup actions are executed with the new template scope.
321
- await this.executeAction(setupAction, context, templateScope)
322
- }
323
- this.events.emitEvent({
324
- type: 'template_setup_completed',
325
- level: 'debug',
326
- data: {
327
- templateName: template.name
328
- }
329
- })
330
- }
331
- }
332
-
333
- // 3. Evaluate template-level skip conditions.
334
- const templateSkipConditions = template.skip_condition
335
- const templateShouldSkip = await this.evaluateSkipConditions(templateSkipConditions, context, templateScope)
336
- if (templateShouldSkip) {
337
- this.events.emitEvent({
338
- type: 'template_skipped',
339
- level: 'info',
340
- data: {
341
- templateName: template.name,
342
- reason: 'condition met'
343
- }
344
- })
345
- // Even if we skip the main actions, we must still process the outputs,
346
- // as they might be pre-computable (e.g., a CREATE2 address).
347
- } else {
348
- // 4. Execute the main actions within the template.
349
- for (const templateAction of template.actions) {
350
- await this.executeAction(templateAction, context, templateScope)
351
- }
352
- }
353
-
354
- // If post-check conditions are enabled, re-evaluate template-level skip conditions with retry to handle RPC propagation lag
355
- if (!this.noPostCheckConditions && template.skip_condition) {
356
- const { retries, delayMs } = this.getPostCheckRetryConfig(context)
357
- const shouldSkip = await this.retryBooleanCheck(
358
- async () => this.evaluateSkipConditions(template.skip_condition!, context, templateScope),
359
- retries,
360
- delayMs
361
- )
362
- if (!shouldSkip) {
363
- // If skip conditions don't evaluate to true after execution, the template failed
364
- throw new Error(`Template "${template.name}" failed post-execution check: skip conditions did not evaluate to true`)
365
- }
366
- }
367
-
368
- // 5. Resolve and store the template's outputs into the global context.
369
- // If the calling action (job action) specified a custom "output" map, that overrides the template outputs.
370
- if ('name' in callingAction) {
371
- const actionName = callingAction.name
372
- const customOutput = (callingAction as any).output
373
- if (customOutput && typeof customOutput === 'object' && !Array.isArray(customOutput)) {
374
- // Custom output map provided by job action: resolve each mapping within the template scope
375
- for (const [key, value] of Object.entries(customOutput)) {
376
- const resolvedOutput = await this.resolver.resolve(value as any, context, templateScope)
377
- const outputKey = `${actionName}.${key}`
378
- context.setOutput(outputKey, resolvedOutput)
379
- this.events.emitEvent({
380
- type: 'output_stored',
381
- level: 'debug',
382
- data: {
383
- outputKey,
384
- value: resolvedOutput
385
- }
386
- })
387
- }
388
- } else if (template.outputs) {
389
- // Default behavior: use template-defined outputs
390
- for (const [key, value] of Object.entries(template.outputs)) {
391
- const resolvedOutput = await this.resolver.resolve(value, context, templateScope)
392
- const outputKey = `${actionName}.${key}`
393
- context.setOutput(outputKey, resolvedOutput)
394
- this.events.emitEvent({
395
- type: 'output_stored',
396
- level: 'debug',
397
- data: {
398
- outputKey,
399
- value: resolvedOutput
400
- }
401
- })
402
- }
403
- }
404
- }
405
-
406
- this.events.emitEvent({
407
- type: 'template_exited',
408
- level: 'debug',
409
- data: {
410
- templateName: template.name
411
- }
412
- })
413
- } finally {
414
- // Restore previous context path
415
- context.setContextPath(previousContextPath)
416
- }
417
- }
418
-
419
- /**
420
- * Executes a primitive, built-in action.
421
- * @param action The primitive action to execute.
422
- * @param context The global execution context.
423
- * @param scope The local resolution scope.
424
- * @param hasCustomOutput Whether custom outputs are specified for this action.
425
- */
426
- private async executePrimitive(
427
- action: Action,
428
- context: ExecutionContext,
429
- scope: ResolutionScope,
430
- hasCustomOutput: boolean = false,
431
- ): Promise<void> {
432
- const actionName = action.name || action.type
433
- this.events.emitEvent({
434
- type: 'primitive_action',
435
- level: 'debug',
436
- data: {
437
- actionType: action.type
438
- }
439
- })
440
-
441
- switch (action.type) {
442
- case 'send-transaction': {
443
- const resolvedTo = await this.resolver.resolve(action.arguments.to, context, scope)
444
- const resolvedData = action.arguments.data ? await this.resolver.resolve(action.arguments.data, context, scope) : '0x'
445
- const resolvedValue = action.arguments.value ? await this.resolver.resolve(action.arguments.value, context, scope) : 0
446
- const resolvedGasMultiplier = action.arguments.gasMultiplier !== undefined ? await this.resolver.resolve(action.arguments.gasMultiplier, context, scope) : undefined
447
-
448
- // Validate and convert types
449
- const to = validateAddress(resolvedTo, actionName)
450
- const data = validateHexData(resolvedData, actionName, 'data')
451
- const value = validateBigNumberish(resolvedValue, actionName, 'value')
452
-
453
- // Validate gas multiplier if provided
454
- let gasMultiplier: number | undefined
455
- if (resolvedGasMultiplier !== undefined) {
456
- if (typeof resolvedGasMultiplier !== 'number' || resolvedGasMultiplier <= 0) {
457
- throw new Error(`Action "${actionName}": gasMultiplier must be a positive number, got: ${resolvedGasMultiplier}`)
458
- }
459
- gasMultiplier = resolvedGasMultiplier
460
- }
461
-
462
- // Prepare transaction parameters
463
- const txParams: any = { to, data, value }
464
-
465
- // Handle gas limit with optional multiplier
466
- const network = context.getNetwork()
467
- const signer = await context.getResolvedSigner()
468
- if (network.gasLimit) {
469
- const baseGasLimit = network.gasLimit
470
- txParams.gasLimit = gasMultiplier ? Math.floor(baseGasLimit * gasMultiplier) : baseGasLimit
471
- } else if (gasMultiplier) {
472
- // If gasMultiplier is specified but no network gasLimit, estimate gas first
473
- const estimatedGas = await signer.estimateGas({ to, data, value })
474
- txParams.gasLimit = Math.floor(Number(estimatedGas) * gasMultiplier)
475
- }
476
-
477
- await this.checkFundsForTransaction(actionName, txParams, context, signer)
478
- const tx = await signer.sendTransaction(txParams)
479
-
480
- this.events.emitEvent({
481
- type: 'transaction_sent',
482
- level: 'info',
483
- data: {
484
- to,
485
- value: value.toString(),
486
- dataPreview: String(data).substring(0, 42),
487
- txHash: tx.hash
488
- }
489
- })
490
-
491
- const receipt = await tx.wait()
492
- if (!receipt || receipt.status !== 1) {
493
- throw new Error(`Transaction for action "${actionName}" failed (reverted). Hash: ${tx.hash}`)
494
- }
495
-
496
- this.events.emitEvent({
497
- type: 'transaction_confirmed',
498
- level: 'info',
499
- data: {
500
- txHash: tx.hash,
501
- blockNumber: receipt.blockNumber
502
- }
503
- })
504
-
505
- if (action.name && !hasCustomOutput) {
506
- context.setOutput(`${action.name}.hash`, tx.hash)
507
- context.setOutput(`${action.name}.receipt`, receipt)
508
- }
509
- break
510
- }
511
- case 'send-signed-transaction': {
512
- const resolvedRawTx = await this.resolver.resolve(action.arguments.transaction, context, scope)
513
-
514
- // Validate and convert type
515
- const rawTx = validateRawTransaction(resolvedRawTx, actionName)
516
-
517
- const tx = await context.provider.broadcastTransaction(rawTx)
518
-
519
- this.events.emitEvent({
520
- type: 'transaction_sent',
521
- level: 'info',
522
- data: {
523
- to: '',
524
- value: '0',
525
- dataPreview: 'signed transaction',
526
- txHash: tx.hash
527
- }
528
- })
529
-
530
- const receipt = await tx.wait()
531
- if (!receipt || receipt.status !== 1) {
532
- throw new Error(`Signed transaction for action "${actionName}" failed (reverted). Hash: ${tx.hash}`)
533
- }
534
-
535
- this.events.emitEvent({
536
- type: 'transaction_confirmed',
537
- level: 'info',
538
- data: {
539
- txHash: tx.hash,
540
- blockNumber: receipt.blockNumber
541
- }
542
- })
543
-
544
- if (action.name && !hasCustomOutput) {
545
- context.setOutput(`${action.name}.hash`, tx.hash)
546
- context.setOutput(`${action.name}.receipt`, receipt)
547
- }
548
- break
549
- }
550
- case 'verify-contract': {
551
- const actionName = action.name || action.type
552
-
553
- // Resolve arguments
554
- const resolvedAddress = await this.resolver.resolve(action.arguments.address, context, scope)
555
- const resolvedContract = await this.resolver.resolve(action.arguments.contract, context, scope)
556
- const resolvedConstructorArgs = action.arguments.constructorArguments
557
- ? await this.resolver.resolve(action.arguments.constructorArguments, context, scope)
558
- : undefined
559
- const resolvedPlatform = action.arguments.platform
560
- ? await this.resolver.resolve(action.arguments.platform, context, scope)
561
- : 'all'
562
-
563
- // Validate inputs
564
- const address = validateAddress(resolvedAddress, actionName)
565
-
566
- if (!resolvedContract || typeof resolvedContract !== 'object') {
567
- throw new Error(`Action "${actionName}": contract must be a Contract object`)
568
- }
569
-
570
- const contract = resolvedContract as Contract
571
-
572
- // Handle platform validation - allow string, array of strings, or 'all'
573
- let platformsToTry: string[]
574
- if (resolvedPlatform === 'all') {
575
- platformsToTry = ['all']
576
- } else if (typeof resolvedPlatform === 'string') {
577
- platformsToTry = [resolvedPlatform]
578
- } else if (Array.isArray(resolvedPlatform)) {
579
- // Validate that all array elements are strings
580
- if (!resolvedPlatform.every(p => typeof p === 'string')) {
581
- throw new Error(`Action "${actionName}": platform array must contain only strings`)
582
- }
583
- platformsToTry = resolvedPlatform
584
- } else {
585
- throw new Error(`Action "${actionName}": platform must be a string, array of strings, or 'all'`)
586
- }
587
-
588
- // Validate that the contract has the necessary information for verification
589
- if (!contract.sourceName) {
590
- throw new Error(`Action "${actionName}": Contract is missing sourceName required for verification`)
591
- }
592
- if (!contract.contractName) {
593
- throw new Error(`Action "${actionName}": Contract is missing contractName required for verification`)
594
- }
595
- if (!contract.compiler) {
596
- throw new Error(`Action "${actionName}": Contract is missing compiler information required for verification`)
597
- }
598
- if (!contract.buildInfoId) {
599
- throw new Error(`Action "${actionName}": Contract is missing buildInfoId required for verification`)
600
- }
601
-
602
- // Validate constructor arguments if provided
603
- let constructorArguments: string | undefined
604
- if (resolvedConstructorArgs !== undefined) {
605
- constructorArguments = validateHexData(resolvedConstructorArgs, actionName, 'constructorArguments')
606
- }
607
-
608
- const network = context.getNetwork()
609
- const contractName = `${contract.sourceName}:${contract.contractName}`
610
-
611
- // Handle platform verification
612
- if (platformsToTry.includes('all')) {
613
- // Handle "all" platform - try all configured platforms for this network
614
- const configuredPlatforms = this.verificationRegistry.getConfiguredPlatforms(network)
615
-
616
- if (configuredPlatforms.length === 0) {
617
- this.events.emitEvent({
618
- type: 'action_skipped',
619
- level: 'warn',
620
- data: {
621
- actionName: actionName,
622
- reason: `No configured verification platforms available for network ${network.name}`
623
- }
624
- })
625
- return
626
- }
627
-
628
- // Try verification on all configured platforms
629
- let anySuccess = false
630
- for (const platform of configuredPlatforms) {
631
- try {
632
- await this.verifyOnSinglePlatform(
633
- platform,
634
- contract,
635
- address,
636
- constructorArguments,
637
- network,
638
- actionName,
639
- contractName,
640
- action,
641
- context,
642
- hasCustomOutput
643
- )
644
- anySuccess = true
645
- } catch (error) {
646
- const errorMessage = error instanceof Error ? error.message : String(error)
647
-
648
- // If ignoreVerifyErrors is enabled, add to warnings and continue
649
- if (this.ignoreVerifyErrors) {
650
- this.verificationWarnings.push({
651
- actionName: actionName,
652
- address,
653
- contractName,
654
- platform: platform.name,
655
- error: errorMessage,
656
- networkName: network.name
657
- })
658
- }
659
-
660
- // Log the error but continue with other platforms
661
- this.events.emitEvent({
662
- type: 'verification_failed',
663
- level: 'warn',
664
- data: {
665
- actionName: actionName,
666
- address,
667
- contractName,
668
- platform: platform.name,
669
- error: errorMessage
670
- }
671
- })
672
- }
673
- }
674
-
675
- if (!anySuccess) {
676
- if (this.ignoreVerifyErrors) {
677
- // Don't throw error if ignoreVerifyErrors is enabled - warnings already collected
678
- this.events.emitEvent({
679
- type: 'verification_skipped',
680
- level: 'warn',
681
- data: {
682
- actionName: actionName,
683
- reason: `Verification failed on all configured platforms for network ${network.name}, but continuing due to --ignore-verify-errors`
684
- }
685
- })
686
- } else {
687
- throw new Error(`Verification failed on all configured platforms for network ${network.name}`)
688
- }
689
- }
690
- } else {
691
- // Handle specific platform(s) verification
692
- let anySuccess = false
693
- for (const platformName of platformsToTry) {
694
- const platform = this.verificationRegistry.get(platformName)
695
- if (!platform) {
696
- throw new Error(`Action "${actionName}": Unsupported verification platform "${platformName}"`)
697
- }
698
-
699
- try {
700
- await this.verifyOnSinglePlatform(
701
- platform,
702
- contract,
703
- address,
704
- constructorArguments,
705
- network,
706
- actionName,
707
- contractName,
708
- action,
709
- context,
710
- hasCustomOutput
711
- )
712
- anySuccess = true
713
- } catch (error) {
714
- const errorMessage = error instanceof Error ? error.message : String(error)
715
-
716
- // If ignoreVerifyErrors is enabled, add to warnings
717
- if (this.ignoreVerifyErrors) {
718
- this.verificationWarnings.push({
719
- actionName: actionName,
720
- address,
721
- contractName,
722
- platform: platform.name,
723
- error: errorMessage,
724
- networkName: network.name
725
- })
726
- }
727
-
728
- // Log the error but continue with other platforms if multiple specified
729
- this.events.emitEvent({
730
- type: 'verification_failed',
731
- level: platformsToTry.length > 1 ? 'warn' : 'error',
732
- data: {
733
- actionName: actionName,
734
- address,
735
- contractName,
736
- platform: platform.name,
737
- error: errorMessage
738
- }
739
- })
740
-
741
- // If only one platform specified, re-throw the error unless ignoreVerifyErrors is enabled
742
- if (platformsToTry.length === 1 && !this.ignoreVerifyErrors) {
743
- throw error
744
- }
745
- }
746
- }
747
-
748
- if (!anySuccess && platformsToTry.length > 1) {
749
- if (this.ignoreVerifyErrors) {
750
- // Don't throw error if ignoreVerifyErrors is enabled - warnings already collected
751
- this.events.emitEvent({
752
- type: 'verification_skipped',
753
- level: 'warn',
754
- data: {
755
- actionName: actionName,
756
- reason: `Verification failed on all specified platforms: ${platformsToTry.join(', ')}, but continuing due to --ignore-verify-errors`
757
- }
758
- })
759
- } else {
760
- throw new Error(`Verification failed on all specified platforms: ${platformsToTry.join(', ')}`)
761
- }
762
- }
763
- }
764
-
765
- break
766
- }
767
- case 'static': {
768
- const resolvedValue = await this.resolver.resolve(action.arguments.value, context, scope)
769
-
770
- if (action.name && !hasCustomOutput) {
771
- context.setOutput(`${action.name}.value`, resolvedValue)
772
- }
773
- break
774
- }
775
- case 'create-contract': {
776
- const resolvedData = await this.resolver.resolve(action.arguments.data, context, scope)
777
- const resolvedValue = action.arguments.value ? await this.resolver.resolve(action.arguments.value, context, scope) : 0
778
- const resolvedGasMultiplier = action.arguments.gasMultiplier !== undefined ? await this.resolver.resolve(action.arguments.gasMultiplier, context, scope) : undefined
779
-
780
- // Validate and convert types
781
- const data = validateHexData(resolvedData, actionName, 'data')
782
- const value = validateBigNumberish(resolvedValue, actionName, 'value')
783
-
784
- // Validate gas multiplier if provided
785
- let gasMultiplier: number | undefined
786
- if (resolvedGasMultiplier !== undefined) {
787
- if (typeof resolvedGasMultiplier !== 'number' || resolvedGasMultiplier <= 0) {
788
- throw new Error(`Action "${actionName}": gasMultiplier must be a positive number, got: ${resolvedGasMultiplier}`)
789
- }
790
- gasMultiplier = resolvedGasMultiplier
791
- }
792
-
793
- // Prepare transaction parameters for contract creation (to: null)
794
- const txParams: any = { to: null, data, value }
795
-
796
- // Handle gas limit with optional multiplier
797
- const network = context.getNetwork()
798
- const signer = await context.getResolvedSigner()
799
- if (network.gasLimit) {
800
- const baseGasLimit = network.gasLimit
801
- txParams.gasLimit = gasMultiplier ? Math.floor(baseGasLimit * gasMultiplier) : baseGasLimit
802
- } else if (gasMultiplier) {
803
- // If gasMultiplier is specified but no network gasLimit, estimate gas first
804
- const estimatedGas = await signer.estimateGas(txParams)
805
- txParams.gasLimit = Math.floor(Number(estimatedGas) * gasMultiplier)
806
- }
807
-
808
- await this.checkFundsForTransaction(actionName, txParams, context, signer)
809
- const tx = await signer.sendTransaction(txParams)
810
-
811
- this.events.emitEvent({
812
- type: 'transaction_sent',
813
- level: 'info',
814
- data: {
815
- to: 'contract creation',
816
- value: value.toString(),
817
- dataPreview: String(data).substring(0, 42),
818
- txHash: tx.hash
819
- }
820
- })
821
-
822
- const receipt = await tx.wait()
823
- if (!receipt || receipt.status !== 1) {
824
- throw new Error(`Contract creation for action "${actionName}" failed (reverted). Hash: ${tx.hash}`)
825
- }
826
-
827
- if (!receipt.contractAddress) {
828
- throw new Error(`Contract creation for action "${actionName}" did not return a contract address. Hash: ${tx.hash}`)
829
- }
830
-
831
- this.events.emitEvent({
832
- type: 'transaction_confirmed',
833
- level: 'info',
834
- data: {
835
- txHash: tx.hash,
836
- blockNumber: receipt.blockNumber
837
- }
838
- })
839
-
840
- this.events.emitEvent({
841
- type: 'contract_created',
842
- level: 'info',
843
- data: {
844
- contractAddress: receipt.contractAddress,
845
- txHash: tx.hash,
846
- blockNumber: receipt.blockNumber
847
- }
848
- })
849
-
850
- if (action.name && !hasCustomOutput) {
851
- context.setOutput(`${action.name}.hash`, tx.hash)
852
- context.setOutput(`${action.name}.receipt`, receipt)
853
- context.setOutput(`${action.name}.address`, receipt.contractAddress)
854
- }
855
- break
856
- }
857
- case 'test-nicks-method': {
858
- if (this.nicksMethodResult !== undefined && !this.allowMultipleNicksMethodTests) {
859
- if (this.nicksMethodResult === false) {
860
- throw new Error(`Nick's method test already failed this run`)
861
- }
862
- this.events.emitEvent({
863
- type: 'debug_info',
864
- level: 'debug',
865
- data: {
866
- message: `Nick's method test already passed this run`,
867
- },
868
- })
869
- break
870
- }
871
-
872
- // Default bytecode if none provided
873
- const defaultBytecode = '0x608060405234801561001057600080fd5b5061013d806100206000396000f3fe60806040526004361061001e5760003560e01c80639c4ae2d014610023575b600080fd5b6100cb6004803603604081101561003957600080fd5b81019060208101813564010000000081111561005457600080fd5b82018360208201111561006657600080fd5b8035906020019184600183028401116401000000008311171561008857600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955050913592506100cd915050565b005b60008183516020850134f56040805173ffffffffffffffffffffffffffffffffffffffff83168152905191925081900360200190a050505056fea264697066735822122033609f614f03931b92d88c309d698449bb77efcd517328d341fa4f923c5d8c7964736f6c63430007060033'
874
-
875
- // Handle case where arguments is undefined (action takes no arguments)
876
- const args = action.arguments || {}
877
- const resolvedBytecode = args.bytecode ? await this.resolver.resolve(args.bytecode, context, scope) : defaultBytecode
878
- const resolvedGasPrice = args.gasPrice ? await this.resolver.resolve(args.gasPrice, context, scope) : undefined
879
- const resolvedGasLimit = args.gasLimit ? await this.resolver.resolve(args.gasLimit, context, scope) : undefined
880
- const resolvedFundingAmount = args.fundingAmount ? await this.resolver.resolve(args.fundingAmount, context, scope) : undefined
881
-
882
- // Validate inputs
883
- const bytecode = validateHexData(resolvedBytecode, actionName, 'bytecode')
884
- const gasPrice = resolvedGasPrice ? validateBigNumberish(resolvedGasPrice, actionName, 'gasPrice') : undefined
885
- const gasLimit = resolvedGasLimit ? validateBigNumberish(resolvedGasLimit, actionName, 'gasLimit') : undefined
886
- const fundingAmount = resolvedFundingAmount ? validateBigNumberish(resolvedFundingAmount, actionName, 'fundingAmount') : undefined
887
-
888
- const success = await this.testNicksMethod(bytecode, context, gasPrice, gasLimit, fundingAmount)
889
- this.nicksMethodResult = success
890
-
891
- if (!success) {
892
- throw new Error(`Nick's method test failed for action "${actionName}"`)
893
- }
894
-
895
- this.events.emitEvent({
896
- type: 'action_completed',
897
- level: 'info',
898
- data: {
899
- actionName: actionName,
900
- result: 'Nick\'s method test passed'
901
- }
902
- })
903
-
904
- if (action.name && !hasCustomOutput) {
905
- context.setOutput(`${action.name}.success`, true)
906
- }
907
- break
908
- }
909
- case 'json-request': {
910
- const resolvedUrl = await this.resolver.resolve(action.arguments.url, context, scope)
911
- const resolvedMethod = action.arguments.method ? await this.resolver.resolve(action.arguments.method, context, scope) : 'GET'
912
- const resolvedHeaders = action.arguments.headers ? await this.resolver.resolve(action.arguments.headers, context, scope) : {}
913
- const resolvedBody = action.arguments.body ? await this.resolver.resolve(action.arguments.body, context, scope) : undefined
914
-
915
- // Validate inputs
916
- if (typeof resolvedUrl !== 'string') {
917
- throw new Error(`Action "${actionName}": url must be a string, got: ${typeof resolvedUrl}`)
918
- }
919
-
920
- if (typeof resolvedMethod !== 'string') {
921
- throw new Error(`Action "${actionName}": method must be a string, got: ${typeof resolvedMethod}`)
922
- }
923
-
924
- if (resolvedHeaders && typeof resolvedHeaders !== 'object') {
925
- throw new Error(`Action "${actionName}": headers must be an object, got: ${typeof resolvedHeaders}`)
926
- }
927
-
928
- try {
929
- // Prepare fetch options
930
- const fetchOptions: RequestInit = {
931
- method: resolvedMethod.toUpperCase(),
932
- headers: {
933
- 'Content-Type': 'application/json',
934
- ...(resolvedHeaders as Record<string, string>)
935
- }
936
- }
937
-
938
- // Add body for non-GET requests
939
- if (resolvedBody !== undefined && resolvedMethod.toUpperCase() !== 'GET') {
940
- fetchOptions.body = JSON.stringify(resolvedBody)
941
- }
942
-
943
- this.events.emitEvent({
944
- type: 'action_started',
945
- level: 'info',
946
- data: {
947
- actionName: actionName,
948
- message: `Making ${resolvedMethod.toUpperCase()} request to ${resolvedUrl}`
949
- }
950
- })
951
-
952
- // Make the HTTP request
953
- const response = await fetch(resolvedUrl, fetchOptions)
954
-
955
- if (!response.ok) {
956
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
957
- }
958
-
959
- // Parse JSON response
960
- const responseData = await response.json()
961
-
962
- this.events.emitEvent({
963
- type: 'action_completed',
964
- level: 'info',
965
- data: {
966
- actionName: actionName,
967
- message: `Request completed successfully (${response.status})`
968
- }
969
- })
970
-
971
- // Store outputs
972
- if (action.name && !hasCustomOutput) {
973
- context.setOutput(`${action.name}.response`, responseData)
974
- context.setOutput(`${action.name}.status`, response.status)
975
- context.setOutput(`${action.name}.statusText`, response.statusText)
976
- }
977
- } catch (error) {
978
- this.events.emitEvent({
979
- type: 'action_failed',
980
- level: 'error',
981
- data: {
982
- actionName: actionName,
983
- error: error instanceof Error ? error.message : String(error)
984
- }
985
- })
986
- throw new Error(`Action "${actionName}" failed: ${error instanceof Error ? error.message : String(error)}`)
987
- }
988
- break
989
- }
990
- case 'assert': {
991
- // Determine the source of ACTUAL value
992
- let actual: any
993
- let describe: string
994
- if (action.arguments.to !== undefined) {
995
- // Use eth_call via the call resolver
996
- actual = await this.resolver.resolve(
997
- { type: 'call', arguments: { to: action.arguments.to, signature: action.arguments.signature, values: action.arguments.values ?? [] } },
998
- context,
999
- scope,
1000
- )
1001
- describe = action.arguments.signature ?? 'call'
1002
- } else {
1003
- // Use the `actual` value resolver
1004
- actual = await this.resolver.resolve(action.arguments.actual, context, scope)
1005
- describe = action.arguments.signature ?? 'value'
1006
- }
1007
-
1008
- // Determine the comparator key and expected value (exactly one required)
1009
- const comparatorKeys = ['eq', 'neq', 'gt', 'lt', 'gte', 'lte']
1010
- const args = action.arguments as any
1011
- const presentKeys = comparatorKeys.filter((key) => args[key] !== undefined)
1012
- if (presentKeys.length === 0) {
1013
- throw new Error(`Action "${actionName}": assert must have exactly one of: ${comparatorKeys.join(', ')}`)
1014
- }
1015
- if (presentKeys.length > 1) {
1016
- throw new Error(`Action "${actionName}": assert must have exactly one comparator, but got: ${presentKeys.join(', ')}`)
1017
- }
1018
- const operation = presentKeys[0]
1019
- let expected: any = args[operation]
1020
-
1021
- // Resolve expected value
1022
- expected = await this.resolver.resolve(expected, context, scope)
1023
-
1024
- // Compare using basic-arithmetic (which returns a boolean for eq/neq/gt/lt/gte/lte)
1025
- const ok = await this.resolver.resolve(
1026
- { type: 'basic-arithmetic', arguments: { operation, values: [actual, expected] } },
1027
- context,
1028
- scope,
1029
- )
1030
-
1031
- if (!ok) {
1032
- const messagePart = action.arguments.message ? `: ${action.arguments.message}` : ''
1033
- throw new Error(`assert failed${messagePart}: ${describe} (actual=${actual}, expected=${expected}, op=${operation})`)
1034
- }
1035
-
1036
- // Store output if named and no custom output
1037
- if (action.name && !hasCustomOutput) {
1038
- context.setOutput(`${action.name}.actual`, actual)
1039
- }
1040
- break
1041
- }
1042
- default:
1043
- throw new Error(`Unknown or unimplemented primitive action type: ${(action as any).type}`)
1044
- }
1045
- }
1046
-
1047
- /**
1048
- * Helper method to verify a contract on a single platform
1049
- * @private
1050
- */
1051
- private async verifyOnSinglePlatform(
1052
- platform: any,
1053
- contract: Contract,
1054
- address: string,
1055
- constructorArguments: string | undefined,
1056
- network: any,
1057
- actionName: string,
1058
- contractName: string,
1059
- action: Action,
1060
- context: ExecutionContext,
1061
- hasCustomOutput: boolean = false
1062
- ): Promise<void> {
1063
- // Check if platform supports this network
1064
- const supportsNetwork = platform.supportsNetwork(network)
1065
- if (!supportsNetwork) {
1066
- this.events.emitEvent({
1067
- type: 'action_skipped',
1068
- level: 'info',
1069
- data: {
1070
- actionName: actionName,
1071
- reason: `Network ${network.name} does not support ${platform.name} verification`
1072
- }
1073
- })
1074
- return
1075
- }
1076
-
1077
- // Check if platform is properly configured
1078
- const isConfigured = platform.isConfigured()
1079
- if (!isConfigured) {
1080
- this.events.emitEvent({
1081
- type: 'action_skipped',
1082
- level: 'warn',
1083
- data: {
1084
- actionName: actionName,
1085
- reason: `Verification skipped: ${platform.getConfigurationRequirements()}`
1086
- }
1087
- })
1088
- return
1089
- }
1090
-
1091
- // Find and load build info
1092
- let buildInfoPath: string | undefined
1093
- for (const sourcePath of contract._sources) {
1094
- if (sourcePath.includes('/build-info/') && sourcePath.endsWith('.json')) {
1095
- buildInfoPath = sourcePath
1096
- break
1097
- }
1098
- }
1099
-
1100
- if (!buildInfoPath) {
1101
- throw new Error(`Action "${actionName}": No build-info file found in contract sources`)
1102
- }
1103
-
1104
- const fs = await import('fs/promises')
1105
- let buildInfoContent: string
1106
- try {
1107
- buildInfoContent = await fs.readFile(buildInfoPath, 'utf-8')
1108
- } catch (error) {
1109
- throw new Error(`Action "${actionName}": Failed to read build info file at ${buildInfoPath}: ${error instanceof Error ? error.message : String(error)}`)
1110
- }
1111
-
1112
- let buildInfo: BuildInfo
1113
- try {
1114
- buildInfo = JSON.parse(buildInfoContent)
1115
- } catch (error) {
1116
- throw new Error(`Action "${actionName}": Failed to parse build info JSON: ${error instanceof Error ? error.message : String(error)}`)
1117
- }
1118
-
1119
- this.events.emitEvent({
1120
- type: 'verification_started',
1121
- level: 'info',
1122
- data: {
1123
- actionName: actionName,
1124
- address,
1125
- contractName,
1126
- platform: platform.name,
1127
- networkName: network.name
1128
- }
1129
- })
1130
-
1131
- try {
1132
- // Use the platform to verify the contract
1133
- const verificationResult = await platform.verifyContract({
1134
- contract,
1135
- buildInfo,
1136
- address,
1137
- constructorArguments,
1138
- network
1139
- })
1140
-
1141
- if (!verificationResult.success) {
1142
- throw new Error(`Verification failed: ${verificationResult.message}`)
1143
- }
1144
-
1145
- // Emit appropriate events based on verification result
1146
- if (verificationResult.isAlreadyVerified) {
1147
- this.events.emitEvent({
1148
- type: 'verification_completed',
1149
- level: 'info',
1150
- data: {
1151
- actionName: actionName,
1152
- address,
1153
- contractName,
1154
- platform: platform.name,
1155
- message: verificationResult.message
1156
- }
1157
- })
1158
- } else {
1159
- this.events.emitEvent({
1160
- type: 'verification_submitted',
1161
- level: 'info',
1162
- data: {
1163
- actionName: actionName,
1164
- platform: platform.name,
1165
- guid: verificationResult.guid || 'N/A',
1166
- message: verificationResult.message
1167
- }
1168
- })
1169
-
1170
- this.events.emitEvent({
1171
- type: 'verification_completed',
1172
- level: 'info',
1173
- data: {
1174
- actionName: actionName,
1175
- address,
1176
- contractName,
1177
- platform: platform.name,
1178
- message: 'Contract verified successfully'
1179
- }
1180
- })
1181
- }
1182
-
1183
- // Set outputs (only for successful verifications)
1184
- if (action.name && !hasCustomOutput) {
1185
- context.setOutput(`${action.name}.verified`, true)
1186
- if (verificationResult.guid) {
1187
- context.setOutput(`${action.name}.guid`, verificationResult.guid)
1188
- }
1189
- }
1190
-
1191
- } catch (error) {
1192
- this.events.emitEvent({
1193
- type: 'verification_failed',
1194
- level: 'error',
1195
- data: {
1196
- actionName: actionName,
1197
- address,
1198
- contractName,
1199
- platform: platform.name,
1200
- error: error instanceof Error ? error.message : String(error)
1201
- }
1202
- })
1203
- throw error
1204
- }
1205
- }
1206
-
1207
- /**
1208
- * Tests Nick's method for EOA deployment
1209
- * Generates a valid ECDSA signature and tests if it can deploy the given bytecode
1210
- * Returns any remaining funds to the original wallet after testing
1211
- */
1212
- private async testNicksMethod(
1213
- bytecode: string,
1214
- context: ExecutionContext,
1215
- gasPrice?: ethers.BigNumberish,
1216
- gasLimit?: ethers.BigNumberish,
1217
- fundingAmount?: ethers.BigNumberish
1218
- ): Promise<boolean> {
1219
- let testResult = false
1220
- let eoaAddress: string | undefined
1221
- let wallet: ethers.HDNodeWallet | ethers.Wallet | undefined
1222
-
1223
- try {
1224
- // Default values
1225
- const defaultGasPrice = gasPrice || ethers.parseUnits('100', 'gwei') // 100 gwei
1226
- const defaultGasLimit = gasLimit || 250000n // Reasonable gas limit for deployment
1227
- const calculatedCost = BigInt(defaultGasPrice.toString()) * BigInt(defaultGasLimit.toString())
1228
- const defaultFundingAmount = fundingAmount || calculatedCost
1229
-
1230
- // Check main signer balance first
1231
- const signer = await context.getResolvedSigner()
1232
- const signerAddress = await signer.getAddress()
1233
- const signerBalance = await context.provider.getBalance(signerAddress)
1234
-
1235
- if (signerBalance < BigInt(defaultFundingAmount.toString())) {
1236
- this.events.emitEvent({
1237
- type: 'action_failed',
1238
- level: 'error',
1239
- data: {
1240
- message: `Insufficient funds: signer has ${ethers.formatEther(signerBalance)} ETH but needs ${ethers.formatEther(defaultFundingAmount)} ETH`
1241
- }
1242
- })
1243
- return false
1244
- }
1245
-
1246
- // Generate a valid ECDSA signature using Nick's method approach
1247
- const result = await this.generateNicksMethodTransaction(bytecode, defaultGasPrice, defaultGasLimit)
1248
- const {signedTx, unsignedTx} = result
1249
- eoaAddress = result.eoaAddress
1250
- wallet = result.wallet
1251
-
1252
- // Simulate the contract creation transaction
1253
- try {
1254
- const simulationTx = {
1255
- ...unsignedTx,
1256
- from: eoaAddress,
1257
- };
1258
-
1259
- if (unsignedTx.gasPrice) {
1260
- // Check gas price
1261
- const gasPrice = await context.provider.getFeeData().then(data => data.gasPrice)
1262
- if (!gasPrice) {
1263
- this.events.emitEvent({
1264
- type: "debug_info",
1265
- level: "debug",
1266
- data: {
1267
- message: `Legacy gas price not available.`,
1268
- },
1269
- });
1270
- } else if (BigInt(unsignedTx.gasPrice.toString()) < gasPrice) {
1271
- this.events.emitEvent({
1272
- type: "debug_info",
1273
- level: "warn",
1274
- data: {
1275
- message: `Gas price (${unsignedTx.gasPrice}) is lower than the current gas price (${gasPrice}). This may cause the transaction to not be mined.`,
1276
- },
1277
- });
1278
- }
1279
- }
1280
-
1281
- if (simulationTx.gasLimit) {
1282
- // Simulate the transaction expected gas usage
1283
- const estimatedGas = await context.provider.estimateGas(simulationTx);
1284
- const estimatedGasStr = estimatedGas.toString();
1285
- const simulationTxGasLimitStr = simulationTx.gasLimit.toString();
1286
- if (estimatedGas > BigInt(simulationTxGasLimitStr)) {
1287
- this.events.emitEvent({
1288
- type: "debug_info",
1289
- level: "warn",
1290
- data: {
1291
- message: `Estimated gas (${estimatedGasStr}) is greater than gas provided in the transaction (${simulationTxGasLimitStr}). This may cause the transaction to revert.`,
1292
- },
1293
- });
1294
- } else {
1295
- this.events.emitEvent({
1296
- type: "debug_info",
1297
- level: "debug",
1298
- data: {
1299
- message: `Estimated gas: ${estimatedGasStr}, Gas provided: ${simulationTxGasLimitStr}`,
1300
- },
1301
- });
1302
- }
1303
- }
1304
- } catch (simulationError) {
1305
- this.events.emitEvent({
1306
- type: "debug_info",
1307
- level: "warn",
1308
- data: {
1309
- message: `Simulation failed: ${
1310
- simulationError instanceof Error
1311
- ? simulationError.message
1312
- : String(simulationError)
1313
- }`,
1314
- },
1315
- });
1316
- // Continue with the test even if simulation fails
1317
- }
1318
-
1319
- this.events.emitEvent({
1320
- type: 'debug_info',
1321
- level: 'debug',
1322
- data: {
1323
- message: `Testing Nick's method with EOA: ${eoaAddress}`
1324
- }
1325
- })
1326
-
1327
- // Check if EOA already has sufficient balance
1328
- const currentBalance = await context.provider.getBalance(eoaAddress)
1329
- const neededFunding = BigInt(defaultFundingAmount.toString()) - currentBalance
1330
-
1331
- if (neededFunding > 0) {
1332
- // Fund the EOA
1333
- this.events.emitEvent({
1334
- type: 'transaction_sent',
1335
- level: 'debug',
1336
- data: {
1337
- to: eoaAddress,
1338
- value: neededFunding.toString(),
1339
- dataPreview: 'funding EOA for Nick\'s method test',
1340
- txHash: 'pending'
1341
- }
1342
- })
1343
-
1344
- this.events.emitEvent({
1345
- type: 'debug_info',
1346
- level: 'debug',
1347
- data: {
1348
- message: `[NICK'S METHOD DEBUG] Sending funding transaction: ${ethers.formatEther(neededFunding)} ETH to ${eoaAddress}`
1349
- }
1350
- })
1351
-
1352
- const signer = await context.getResolvedSigner()
1353
- const fundingTx = await signer.sendTransaction({
1354
- to: eoaAddress,
1355
- value: neededFunding
1356
- })
1357
-
1358
- this.events.emitEvent({
1359
- type: 'debug_info',
1360
- level: 'debug',
1361
- data: {
1362
- message: `[NICK'S METHOD DEBUG] Funding transaction sent: ${fundingTx.hash}, waiting for confirmation...`
1363
- }
1364
- })
1365
-
1366
- const fundingReceipt = await fundingTx.wait()
1367
-
1368
- this.events.emitEvent({
1369
- type: 'transaction_confirmed',
1370
- level: 'debug',
1371
- data: {
1372
- txHash: fundingTx.hash,
1373
- blockNumber: fundingReceipt?.blockNumber || 0
1374
- }
1375
- })
1376
-
1377
- this.events.emitEvent({
1378
- type: 'debug_info',
1379
- level: 'debug',
1380
- data: {
1381
- message: `[NICK'S METHOD DEBUG] Funded EOA ${eoaAddress} with ${ethers.formatEther(neededFunding)} ETH, receipt status: ${fundingReceipt?.status}`
1382
- }
1383
- })
1384
-
1385
- if (!fundingReceipt || fundingReceipt.status !== 1) {
1386
- this.events.emitEvent({
1387
- type: 'action_failed',
1388
- level: 'error',
1389
- data: {
1390
- message: `[NICK'S METHOD DEBUG] Funding transaction failed! Hash: ${fundingTx.hash}, Status: ${fundingReceipt?.status}`
1391
- }
1392
- })
1393
- return false
1394
- }
1395
- } else {
1396
- this.events.emitEvent({
1397
- type: 'debug_info',
1398
- level: 'debug',
1399
- data: {
1400
- message: `[NICK'S METHOD DEBUG] EOA already has sufficient balance, skipping funding`
1401
- }
1402
- })
1403
- }
1404
-
1405
- // Try to broadcast the raw transaction
1406
- this.events.emitEvent({
1407
- type: 'debug_info',
1408
- level: 'debug',
1409
- data: {
1410
- message: `[NICK'S METHOD DEBUG] Broadcasting Nick's method transaction. RawTx: ${signedTx.substring(0, 100)}...`
1411
- }
1412
- })
1413
-
1414
- const deployTx = await context.provider.broadcastTransaction(signedTx)
1415
-
1416
- this.events.emitEvent({
1417
- type: 'debug_info',
1418
- level: 'debug',
1419
- data: {
1420
- message: `[NICK'S METHOD DEBUG] Transaction broadcasted successfully. Hash: ${deployTx.hash}, waiting for confirmation...`
1421
- }
1422
- })
1423
-
1424
- const receipt = await deployTx.wait()
1425
-
1426
- this.events.emitEvent({
1427
- type: 'debug_info',
1428
- level: 'debug',
1429
- data: {
1430
- message: `[NICK'S METHOD DEBUG] Transaction receipt received. Status: ${receipt?.status}, ContractAddress: ${receipt?.contractAddress}, BlockNumber: ${receipt?.blockNumber}`
1431
- }
1432
- })
1433
-
1434
- if (receipt && receipt.status === 1) {
1435
- this.events.emitEvent({
1436
- type: 'transaction_confirmed',
1437
- level: 'info',
1438
- data: {
1439
- txHash: deployTx.hash,
1440
- blockNumber: receipt.blockNumber || 0
1441
- }
1442
- })
1443
-
1444
- this.events.emitEvent({
1445
- type: 'debug_info',
1446
- level: 'debug',
1447
- data: {
1448
- message: `[NICK'S METHOD DEBUG] Nick's method test successful - contract deployed at ${receipt.contractAddress}`
1449
- }
1450
- })
1451
- testResult = true
1452
- } else {
1453
- this.events.emitEvent({
1454
- type: 'action_failed',
1455
- level: 'error',
1456
- data: {
1457
- message: `[NICK'S METHOD DEBUG] Nick's method test failed - transaction reverted or failed. Hash: ${deployTx.hash}, Status: ${receipt?.status}`
1458
- }
1459
- })
1460
- testResult = false
1461
- }
1462
- } catch (error) {
1463
- this.events.emitEvent({
1464
- type: 'action_failed',
1465
- level: 'error',
1466
- data: {
1467
- message: `[NICK'S METHOD DEBUG] Nick's method test failed with error: ${error instanceof Error ? error.message : String(error)}`
1468
- }
1469
- })
1470
-
1471
- // Log additional error details for debugging
1472
- if (error instanceof Error && error.stack) {
1473
- this.events.emitEvent({
1474
- type: 'action_failed',
1475
- level: 'debug',
1476
- data: {
1477
- message: `[NICK'S METHOD DEBUG] Error stack trace: ${error.stack}`
1478
- }
1479
- })
1480
- }
1481
-
1482
- testResult = false
1483
- } finally {
1484
- // Always try to return remaining funds to the original wallet
1485
- if (eoaAddress && wallet) {
1486
- try {
1487
- await this.returnRemainingFunds(eoaAddress, wallet, context)
1488
- } catch (error) {
1489
- // Log the error but don't fail the main test
1490
- this.events.emitEvent({
1491
- type: 'action_failed',
1492
- level: 'warn',
1493
- data: {
1494
- message: `Failed to return remaining funds from EOA ${eoaAddress}: ${error instanceof Error ? error.message : String(error)}`
1495
- }
1496
- })
1497
- }
1498
- }
1499
- }
1500
-
1501
- return testResult
1502
- }
1503
-
1504
- /**
1505
- * Generates a raw transaction and EOA address using Nick's method approach
1506
- */
1507
- private async generateNicksMethodTransaction(
1508
- bytecode: string,
1509
- gasPrice: ethers.BigNumberish,
1510
- gasLimit: ethers.BigNumberish
1511
- ): Promise<{ unsignedTx: ethers.TransactionRequest; signedTx: string; eoaAddress: string; wallet: ethers.HDNodeWallet }> {
1512
- // Generate a random private key for the test
1513
- const wallet = ethers.Wallet.createRandom()
1514
-
1515
- // Create unsigned transaction
1516
- const unsignedTx: ethers.TransactionRequest = {
1517
- type: 0, // Legacy transaction
1518
- chainId: 0, // Nick's method uses chainId 0
1519
- nonce: 0,
1520
- gasPrice: gasPrice,
1521
- gasLimit: gasLimit,
1522
- to: null, // Contract creation
1523
- value: 0,
1524
- data: bytecode
1525
- }
1526
-
1527
- // Sign the transaction
1528
- const signedTx = await wallet.signTransaction(unsignedTx)
1529
-
1530
- // Parse the signed transaction to get the EOA address
1531
- const parsedTx = ethers.Transaction.from(signedTx)
1532
- const eoaAddress = parsedTx.from!
1533
-
1534
- return {
1535
- unsignedTx,
1536
- signedTx,
1537
- eoaAddress,
1538
- wallet,
1539
- }
1540
- }
1541
-
1542
- /**
1543
- * Returns any remaining funds from the test EOA back to the original wallet
1544
- */
1545
- private async returnRemainingFunds(
1546
- eoaAddress: string,
1547
- wallet: ethers.HDNodeWallet | ethers.Wallet,
1548
- context: ExecutionContext
1549
- ): Promise<void> {
1550
- // Check remaining balance in the test EOA
1551
- const remainingBalance = await context.provider.getBalance(eoaAddress)
1552
-
1553
- if (remainingBalance <= 0n) {
1554
- // No funds to return
1555
- return
1556
- }
1557
-
1558
- // Connect the wallet to the provider to send transactions
1559
- const connectedWallet = wallet.connect(context.provider)
1560
-
1561
- // Estimate gas for a simple transfer
1562
- const feeData = await context.provider.getFeeData()
1563
- const txGas = feeData.maxFeePerGas ? {
1564
- maxFeePerGas: feeData.maxFeePerGas,
1565
- maxPriorityFeePerGas: feeData.maxPriorityFeePerGas || ethers.parseUnits('20', 'gwei')
1566
- } : {
1567
- gasPrice: feeData.gasPrice || undefined,
1568
- }
1569
- const effectiveGasPrice = txGas.maxFeePerGas || txGas.gasPrice
1570
- if (!effectiveGasPrice) {
1571
- this.events.emitEvent({
1572
- type: 'action_failed',
1573
- level: 'error',
1574
- data: {
1575
- message: `No gas price available`
1576
- }
1577
- })
1578
- return
1579
- }
1580
- const gasLimit = 21000n // Standard gas limit for ETH transfer
1581
- const gasCost = effectiveGasPrice * gasLimit
1582
-
1583
- // Check if we have enough balance to cover gas costs
1584
- if (remainingBalance <= gasCost) {
1585
- this.events.emitEvent({
1586
- type: 'action_info',
1587
- level: 'debug',
1588
- data: {
1589
- message: `Remaining balance ${ethers.formatEther(remainingBalance)} ETH is insufficient to cover gas costs for fund return`
1590
- }
1591
- })
1592
- return
1593
- }
1594
-
1595
- // Calculate amount to send (balance minus gas costs)
1596
- const amountToSend = remainingBalance - gasCost
1597
-
1598
- this.events.emitEvent({
1599
- type: 'transaction_sent',
1600
- level: 'debug',
1601
- data: {
1602
- to: await (await context.getResolvedSigner()).getAddress(),
1603
- value: amountToSend.toString(),
1604
- dataPreview: 'returning remaining funds from Nick\'s method test',
1605
- txHash: 'pending'
1606
- }
1607
- })
1608
-
1609
- // Send the remaining funds back to the original signer
1610
- const returnTx = await connectedWallet.sendTransaction({
1611
- to: await (await context.getResolvedSigner()).getAddress(),
1612
- value: amountToSend,
1613
- gasLimit: gasLimit,
1614
- ...txGas,
1615
- })
1616
-
1617
- await returnTx.wait()
1618
-
1619
- this.events.emitEvent({
1620
- type: 'transaction_confirmed',
1621
- level: 'debug',
1622
- data: {
1623
- txHash: returnTx.hash,
1624
- blockNumber: (await returnTx.wait())?.blockNumber || 0
1625
- }
1626
- })
1627
-
1628
- this.events.emitEvent({
1629
- type: 'debug_info',
1630
- level: 'debug',
1631
- data: {
1632
- message: `Returned ${ethers.formatEther(amountToSend)} ETH from test EOA ${eoaAddress} to original wallet`
1633
- }
1634
- })
1635
- }
1636
-
1637
- /**
1638
- * Retries a boolean-producing async check to mitigate transient RPC state lag after transactions.
1639
- * Returns true on first successful check; otherwise waits delayMs and retries up to retries times.
1640
- */
1641
- private async retryBooleanCheck(checkFn: () => Promise<boolean>, retries: number = 3, delayMs: number = 2000): Promise<boolean> {
1642
- // Throttle debug logging: log first, 25%, 50%, 75%, and final attempt
1643
- const milestones = new Set<number>()
1644
- const total = retries + 1
1645
- milestones.add(1)
1646
- milestones.add(Math.max(1, Math.floor(total * 0.25)))
1647
- milestones.add(Math.max(1, Math.floor(total * 0.5)))
1648
- milestones.add(Math.max(1, Math.floor(total * 0.75)))
1649
- milestones.add(total)
1650
-
1651
- for (let attempt = 0; attempt < total; attempt++) {
1652
- try {
1653
- const result = await checkFn()
1654
- if (result) {
1655
- return true
1656
- }
1657
- if (milestones.has(attempt + 1)) {
1658
- this.events.emitEvent({
1659
- type: 'debug_info',
1660
- level: 'debug',
1661
- data: {
1662
- message: `Post-execution check returned false (attempt ${attempt + 1}/${total}).`
1663
- }
1664
- })
1665
- }
1666
- } catch (err) {
1667
- if (milestones.has(attempt + 1)) {
1668
- this.events.emitEvent({
1669
- type: 'debug_info',
1670
- level: 'debug',
1671
- data: {
1672
- message: `Post-execution check threw error (attempt ${attempt + 1}/${total}): ${err instanceof Error ? err.message : String(err)}`
1673
- }
1674
- })
1675
- }
1676
- }
1677
- if (attempt < retries) {
1678
- await new Promise(res => setTimeout(res, delayMs))
1679
- }
1680
- }
1681
- return false
1682
- }
1683
-
1684
- /**
1685
- * Evaluates a list of conditions and returns true if any of them are met.
1686
- */
1687
- public async evaluateSkipConditions(
1688
- conditions: Condition[] | undefined,
1689
- context: ExecutionContext,
1690
- scope: ResolutionScope,
1691
- ): Promise<boolean> {
1692
- if (!conditions || conditions.length === 0) {
1693
- return false
1694
- }
1695
- for (const condition of conditions) {
1696
- const shouldSkip = await this.resolver.resolve(condition, context, scope)
1697
- if (shouldSkip) {
1698
- return true
1699
- }
1700
- }
1701
- return false
1702
- }
1703
-
1704
- /**
1705
- * Creates a topological sort of actions within a job based on their `depends_on` fields.
1706
- */
1707
- private topologicalSortActions(job: Job): string[] {
1708
- const sorted: string[] = []
1709
- const graph = new Map<string, Set<string>>()
1710
- const inDegree = new Map<string, number>()
1711
- const actionMap = new Map(job.actions.map(a => [a.name, a]))
1712
-
1713
- // Initialize graph and in-degrees
1714
- for (const action of job.actions) {
1715
- graph.set(action.name, new Set(action.depends_on || []))
1716
- inDegree.set(action.name, 0)
1717
- }
1718
-
1719
- // Calculate in-degrees and validate dependencies
1720
- for (const [actionName, dependencies] of graph.entries()) {
1721
- for (const depName of dependencies) {
1722
- if (!actionMap.has(depName)) {
1723
- throw new Error(`Action "${actionName}" in job "${job.name}" has an invalid dependency on "${depName}", which does not exist.`)
1724
- }
1725
- inDegree.set(actionName, (inDegree.get(actionName) ?? 0) + 1)
1726
- }
1727
- }
1728
-
1729
- // Initialize queue with actions having an in-degree of 0
1730
- const queue = Array.from(inDegree.entries())
1731
- .filter(([, degree]) => degree === 0)
1732
- .map(([name]) => name)
1733
-
1734
- // Process the queue
1735
- while (queue.length > 0) {
1736
- const currentName = queue.shift()!
1737
- sorted.push(currentName)
1738
-
1739
- // Find all actions that depend on the current one
1740
- for (const [actionName, dependencies] of graph.entries()) {
1741
- if (dependencies.has(currentName)) {
1742
- const newDegree = (inDegree.get(actionName) ?? 1) - 1
1743
- inDegree.set(actionName, newDegree)
1744
- if (newDegree === 0) {
1745
- queue.push(actionName)
1746
- }
1747
- }
1748
- }
1749
- }
1750
-
1751
- if (sorted.length !== job.actions.length) {
1752
- throw new Error(`Circular dependency detected among actions in job "${job.name}".`)
1753
- }
1754
-
1755
- return sorted
1756
- }
1757
-
1758
- /**
1759
- * Checks if the signer has enough funds to cover the estimated cost of the transaction.
1760
- * Returns true if the signer has enough funds, false if the signer does not have enough funds, and null if no gas price is available.
1761
- */
1762
- private async checkFundsForTransaction(actionName: string, txParams: ethers.TransactionRequest, context: ExecutionContext, signer: ethers.Signer): Promise<boolean | null> {
1763
- try {
1764
- const gasPrice = txParams.gasPrice || await context.provider.getFeeData().then(data => data.gasPrice)
1765
- if (!gasPrice) {
1766
- this.events.emitEvent({
1767
- type: 'debug_info',
1768
- level: 'warn',
1769
- data: {
1770
- actionName: actionName,
1771
- message: `No gas price available`
1772
- }
1773
- })
1774
- return null
1775
- }
1776
- const gasLimit = txParams.gasLimit || await signer.estimateGas(txParams)
1777
- const requiredETH = BigInt(gasLimit) * BigInt(gasPrice)
1778
- const signerBalance = await context.provider.getBalance(await signer.getAddress())
1779
- this.events.emitEvent({
1780
- type: 'debug_info',
1781
- level: 'debug',
1782
- data: {
1783
- actionName: actionName,
1784
- message: `Transaction ${txParams.gasLimit ? 'set' : 'estimated'} gas limit: ${gasLimit}, ${txParams.gasPrice ? 'set' : 'estimated'} gas price: ${ethers.formatUnits(gasPrice, 'gwei')} gwei, required ETH: ${ethers.formatEther(requiredETH)}`
1785
- }
1786
- })
1787
- if (signerBalance < requiredETH) {
1788
- this.events.emitEvent({
1789
- type: 'debug_info',
1790
- level: 'warn',
1791
- data: {
1792
- actionName: actionName,
1793
- message: `Insufficient funds: signer has ${ethers.formatEther(signerBalance)} ETH but estimated cost is ${ethers.formatEther(requiredETH)} ETH`
1794
- }
1795
- })
1796
- return false
1797
- } else {
1798
- return true
1799
- }
1800
- } catch (error) {
1801
- this.events.emitEvent({
1802
- type: 'debug_info',
1803
- level: 'warn',
1804
- data: {
1805
- actionName: actionName,
1806
- message: "Error checking signer balance: " + (error instanceof Error ? error.message : String(error))
1807
- }
1808
- })
1809
- }
1810
- return null
1811
- }
1812
-
1813
- /**
1814
- * Get all verification warnings that were collected when ignoreVerifyErrors is enabled
1815
- */
1816
- public getVerificationWarnings(): Array<{
1817
- actionName: string
1818
- address: string
1819
- contractName: string
1820
- platform: string
1821
- error: string
1822
- jobName?: string
1823
- networkName?: string
1824
- }> {
1825
- return [...this.verificationWarnings]
1826
- }
1827
-
1828
- /**
1829
- * Clear verification warnings (useful for testing)
1830
- */
1831
- public clearVerificationWarnings(): void {
1832
- this.verificationWarnings = []
1833
- }
1834
- }