@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,2005 +0,0 @@
1
- import { ethers } from 'ethers'
2
- import { ExecutionEngine } from '../engine'
3
- import { ExecutionContext } from '../context'
4
- import { ContractRepository } from '../../contracts/repository'
5
- import { Job, Template, JobAction, Action, Network } from '../../types'
6
- import { VerificationPlatformRegistry } from '../../verification/etherscan'
7
-
8
- // Test constants
9
- const TEST_ADDRESSES = {
10
- // Standard anvil addresses for testing
11
- DEPLOYER: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // First anvil account
12
- RECIPIENT_1: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Second anvil account
13
- RECIPIENT_2: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', // Third anvil account
14
- RECIPIENT_3: '0x90F79bf6EB2c4f870365E785982E1f101E93b906', // Fourth anvil account
15
- RECIPIENT_4: '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65', // Fifth anvil account
16
- RECIPIENT_5: '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc', // Sixth anvil account
17
- CONTRACT_ADDRESS: '0x5FbDB2315678afecb367f032d93F642f64180aa3', // Standard contract address
18
- DUMMY_ADDRESS: '0x1234567890123456789012345678901234567890' // For mock tests
19
- } as const
20
-
21
- const TEST_BYTECODES = {
22
- // Valid contract bytecode that works for all tests
23
- SIMPLE_CONTRACT: '0x6080604052348015600e575f5ffd5b5060c180601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c806390c52443146034578063d09de08a14604d575b5f5ffd5b603b5f5481565b60405190815260200160405180910390f35b60536055565b005b5f805490806061836068565b9190505550565b5f60018201608457634e487b7160e01b5f52601160045260245ffd5b506001019056fea264697066735822122061c8cc43c72d6b23b16f7a7337dd15b93d71eb94a9d5247911e39f486e1f94f964736f6c634300081e0033',
24
- // Simple contract that returns 42
25
- SIMPLE_RETURN_42: '0x6080604052348015600f57600080fd5b5060b68061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063a87d942c14602d575b600080fd5b60336035565b005b6000602a9050909156fea2646970667358221220d1b0e2d6c9f3e8a6f5b8d2e3a4c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c556',
26
- // Mini contract with multiplication function
27
- MINI_CONTRACT: '0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80636df5b97a14610038578063f8a8fd6d14610068575b5f5ffd5b610052600480360381019061004d91906100da565b610086565b60405161005f9190610127565b60405180910390f35b61007061009b565b60405161007d9190610127565b60405180910390f35b5f8183610093919061016d565b905092915050565b5f602a905090565b5f5ffd5b5f819050919050565b6100b9816100a7565b81146100c3575f5ffd5b50565b5f813590506100d4816100b0565b92915050565b5f5f604083850312156100f0576100ef6100a3565b5b5f6100fd858286016100c6565b925050602061010e858286016100c6565b9150509250929050565b610121816100a7565b82525050565b5f60208201905061013a5f830184610118565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610177826100a7565b9150610182836100a7565b9250828202610190816100a7565b915082820484148315176101a7576101a6610140565b5b509291505056fea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033',
28
- // Minimal deployment bytecode
29
- MINIMAL_DEPLOY: '0x608060405234801561000f575f5ffd5b50603e80601c5f395ff3fe60806040525f80fdfea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033',
30
- // Broken bytecode
31
- BROKEN_BYTECODE: '0xff',
32
- } as const
33
-
34
- const TEST_VALUES = {
35
- ONE_ETH: '1000000000000000000',
36
- HALF_ETH: '500000000000000000',
37
- TWO_ETH: '2000000000000000000',
38
- SMALL_AMOUNT: '1000000000000000000' // 1 ETH for testing
39
- } as const
40
-
41
- const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' // First anvil account
42
-
43
- describe('ExecutionEngine', () => {
44
- let engine: ExecutionEngine
45
- let context: ExecutionContext
46
- let mockNetwork: Network
47
- let mockRegistry: ContractRepository
48
- let templates: Map<string, Template>
49
- let anvilProvider: ethers.JsonRpcProvider
50
-
51
- beforeAll(async () => {
52
- // Allow configuring RPC URL via environment variable for CI
53
- const rpcUrl = process.env.RPC_URL || 'http://127.0.0.1:8545'
54
- mockNetwork = { name: 'testnet', chainId: 999, rpcUrl }
55
-
56
- // Try to connect to the node, fail immediately if not available
57
- const provider = new ethers.JsonRpcProvider(rpcUrl)
58
- await provider.getNetwork()
59
- })
60
-
61
- beforeEach(async () => {
62
- const rpcUrl = process.env.RPC_URL || 'http://127.0.0.1:8545'
63
- anvilProvider = new ethers.JsonRpcProvider(rpcUrl)
64
-
65
- mockRegistry = new ContractRepository()
66
- context = new ExecutionContext(mockNetwork, TEST_PRIVATE_KEY, mockRegistry)
67
-
68
- // Initialize templates map
69
- templates = new Map()
70
-
71
- // Create empty verification registry for tests
72
- const verificationRegistry = new VerificationPlatformRegistry()
73
- engine = new ExecutionEngine(templates, { verificationRegistry })
74
- })
75
-
76
- afterEach(async () => {
77
- // Clean up providers to prevent hanging connections
78
- if (anvilProvider) {
79
- try {
80
- if (anvilProvider.destroy) {
81
- await anvilProvider.destroy()
82
- }
83
- } catch (error) {
84
- // Ignore cleanup errors
85
- }
86
- }
87
-
88
- if (context) {
89
- try {
90
- await context.dispose()
91
- } catch (error) {
92
- // Ignore cleanup errors
93
- }
94
- }
95
- })
96
-
97
- describe('executeJob', () => {
98
- it('should execute a simple job with no dependencies', async () => {
99
- const job: Job = {
100
- name: 'simple-job',
101
- version: '1.0.0',
102
- actions: [
103
- {
104
- name: 'send-eth',
105
- template: 'send-transaction',
106
- arguments: {
107
- to: TEST_ADDRESSES.RECIPIENT_1,
108
- value: TEST_VALUES.ONE_ETH,
109
- data: '0x'
110
- }
111
- }
112
- ]
113
- }
114
-
115
- await expect(engine.executeJob(job, context)).resolves.not.toThrow()
116
-
117
- // Check that output was stored
118
- expect(context.getOutput('send-eth.hash')).toBeDefined()
119
- expect(context.getOutput('send-eth.receipt')).toBeDefined()
120
- })
121
-
122
- it('should execute actions in dependency order', async () => {
123
- const executionOrder: string[] = []
124
-
125
- // Mock the executeAction method to track execution order
126
- const originalExecuteAction = (engine as any).executeAction
127
- ;(engine as any).executeAction = async function(action: JobAction | Action, ctx: ExecutionContext, scope: any) {
128
- const actionName = 'name' in action ? action.name : action.type
129
- if (actionName) {
130
- executionOrder.push(actionName)
131
- }
132
- // For this test, just track execution - don't actually execute
133
- }
134
-
135
- const job: Job = {
136
- name: 'dependency-job',
137
- version: '1.0.0',
138
- actions: [
139
- {
140
- name: 'action-c',
141
- template: 'send-transaction',
142
- arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
143
- depends_on: ['action-a', 'action-b']
144
- },
145
- {
146
- name: 'action-a',
147
- template: 'send-transaction',
148
- arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' }
149
- },
150
- {
151
- name: 'action-b',
152
- template: 'send-transaction',
153
- arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
154
- depends_on: ['action-a']
155
- }
156
- ]
157
- }
158
-
159
- await engine.executeJob(job, context)
160
-
161
- // Restore original method
162
- ;(engine as any).executeAction = originalExecuteAction
163
-
164
- expect(executionOrder).toEqual(['action-a', 'action-b', 'action-c'])
165
- })
166
-
167
- it('should throw on circular dependencies within a job', async () => {
168
- const job: Job = {
169
- name: 'circular-job',
170
- version: '1.0.0',
171
- actions: [
172
- {
173
- name: 'action-a',
174
- template: 'send-transaction',
175
- arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
176
- depends_on: ['action-b']
177
- },
178
- {
179
- name: 'action-b',
180
- template: 'send-transaction',
181
- arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
182
- depends_on: ['action-a']
183
- }
184
- ]
185
- }
186
-
187
- await expect(engine.executeJob(job, context)).rejects.toThrow('Circular dependency detected among actions in job "circular-job".')
188
- })
189
-
190
- it('should throw on invalid dependencies within a job', async () => {
191
- const job: Job = {
192
- name: 'invalid-dep-job',
193
- version: '1.0.0',
194
- actions: [
195
- {
196
- name: 'action-a',
197
- template: 'send-transaction',
198
- arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
199
- depends_on: ['non-existent-action']
200
- }
201
- ]
202
- }
203
-
204
- await expect(engine.executeJob(job, context)).rejects.toThrow('Action "action-a" in job "invalid-dep-job" has an invalid dependency on "non-existent-action", which does not exist.')
205
- })
206
- })
207
-
208
- describe('executeAction', () => {
209
- it('should skip action when skip condition is met', async () => {
210
- // Set up context to make condition true
211
- context.setOutput('should_skip', 1)
212
-
213
- const action: JobAction = {
214
- name: 'skipped-action',
215
- template: 'send-transaction',
216
- arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
217
- skip_condition: [{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }]
218
- }
219
-
220
- await (engine as any).executeAction(action, context, new Map())
221
-
222
- // Should not have any outputs since it was skipped
223
- expect(() => context.getOutput('skipped-action.hash')).toThrow()
224
- })
225
-
226
- it('should execute action when skip condition is not met', async () => {
227
- context.setOutput('should_skip', 0)
228
-
229
- const action: JobAction = {
230
- name: 'executed-action',
231
- template: 'send-transaction',
232
- arguments: {
233
- to: TEST_ADDRESSES.RECIPIENT_1,
234
- value: TEST_VALUES.ONE_ETH,
235
- data: '0x'
236
- },
237
- skip_condition: [{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }]
238
- }
239
-
240
- await (engine as any).executeAction(action, context, new Map())
241
-
242
- // Should have outputs since it was executed
243
- expect(context.getOutput('executed-action.hash')).toBeDefined()
244
- })
245
-
246
- it('should call executeTemplate for template actions', async () => {
247
- const template: Template = {
248
- name: 'test-template',
249
- actions: [
250
- {
251
- type: 'send-transaction',
252
- name: 'param-action',
253
- arguments: {
254
- to: TEST_ADDRESSES.RECIPIENT_1,
255
- value: TEST_VALUES.ONE_ETH,
256
- data: '0x'
257
- }
258
- }
259
- ]
260
- }
261
- templates.set('test-template', template)
262
-
263
- const action: JobAction = {
264
- name: 'template-action',
265
- template: 'test-template',
266
- arguments: {}
267
- }
268
-
269
- await (engine as any).executeAction(action, context, new Map())
270
- // If no error thrown, template was found and executed
271
- })
272
-
273
- it('should call executePrimitive for primitive actions', async () => {
274
- const action: Action = {
275
- type: 'send-transaction',
276
- name: 'primitive-action',
277
- arguments: {
278
- to: TEST_ADDRESSES.RECIPIENT_1,
279
- value: TEST_VALUES.ONE_ETH,
280
- data: '0x'
281
- }
282
- }
283
-
284
- await (engine as any).executeAction(action, context, new Map())
285
-
286
- expect(context.getOutput('primitive-action.hash')).toBeDefined()
287
- })
288
- })
289
-
290
- describe('executeTemplate', () => {
291
- it('should execute template with setup block', async () => {
292
- // Mock the executeAction method to track execution order instead of sending real transactions
293
- const executedActions: string[] = []
294
- const originalExecuteAction = (engine as any).executeAction
295
- ;(engine as any).executeAction = async function(action: JobAction | Action, ctx: ExecutionContext, scope: any) {
296
- const actionName = 'name' in action ? action.name : action.type
297
- if (actionName) {
298
- executedActions.push(actionName)
299
- // Mock successful execution by setting outputs
300
- if (actionName === 'setup-action') {
301
- ctx.setOutput('setup-action.hash', 'mock-setup-hash')
302
- ctx.setOutput('setup-action.receipt', { status: 1, blockNumber: 100 })
303
- } else if (actionName === 'main-action') {
304
- ctx.setOutput('main-action.hash', 'mock-main-hash')
305
- ctx.setOutput('main-action.receipt', { status: 1, blockNumber: 101 })
306
- }
307
- }
308
- }
309
-
310
- const template: Template = {
311
- name: 'template-with-setup',
312
- setup: {
313
- actions: [
314
- {
315
- type: 'send-transaction',
316
- name: 'setup-action',
317
- arguments: {
318
- to: TEST_ADDRESSES.RECIPIENT_2,
319
- value: TEST_VALUES.HALF_ETH,
320
- data: '0x'
321
- }
322
- }
323
- ]
324
- },
325
- actions: [
326
- {
327
- type: 'send-transaction',
328
- name: 'main-action',
329
- arguments: {
330
- to: TEST_ADDRESSES.RECIPIENT_3,
331
- value: TEST_VALUES.ONE_ETH,
332
- data: '0x'
333
- }
334
- }
335
- ]
336
- }
337
- templates.set('template-with-setup', template)
338
-
339
- const callingAction: JobAction = {
340
- name: 'test-call',
341
- template: 'template-with-setup',
342
- arguments: {}
343
- }
344
-
345
- await (engine as any).executeTemplate(callingAction, 'template-with-setup', context)
346
-
347
- // Restore original method
348
- ;(engine as any).executeAction = originalExecuteAction
349
-
350
- // Verify setup action executed before main action
351
- expect(executedActions).toEqual(['setup-action', 'main-action'])
352
-
353
- // Both setup and main actions should have executed
354
- expect(context.getOutput('setup-action.hash')).toBeDefined()
355
- expect(context.getOutput('main-action.hash')).toBeDefined()
356
- })
357
-
358
- it('should skip template actions when template skip condition is met', async () => {
359
- context.setOutput('skip_template', 1)
360
-
361
- const template: Template = {
362
- name: 'skippable-template',
363
- skip_condition: [{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{skip_template}}', 1] } }],
364
- actions: [
365
- {
366
- type: 'send-transaction',
367
- name: 'skipped-main-action',
368
- arguments: {
369
- to: TEST_ADDRESSES.RECIPIENT_1,
370
- value: TEST_VALUES.ONE_ETH,
371
- data: '0x'
372
- }
373
- }
374
- ]
375
- }
376
- templates.set('skippable-template', template)
377
-
378
- const callingAction: JobAction = {
379
- name: 'test-call',
380
- template: 'skippable-template',
381
- arguments: {}
382
- }
383
-
384
- await (engine as any).executeTemplate(callingAction, 'skippable-template', context)
385
-
386
- // Main action should not have executed
387
- expect(() => context.getOutput('skipped-main-action.hash')).toThrow()
388
- })
389
-
390
- it('should skip template actions when setup skip condition is met', async () => {
391
- context.setOutput('skip_setup', 1)
392
-
393
- const template: Template = {
394
- name: 'skippable-setup-template',
395
- setup: {
396
- skip_condition: [{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{skip_setup}}', 1] } }],
397
- actions: [
398
- {
399
- type: 'send-transaction',
400
- name: 'setup-action',
401
- arguments: {
402
- to: TEST_ADDRESSES.RECIPIENT_4,
403
- value: TEST_VALUES.ONE_ETH,
404
- data: '0x'
405
- }
406
- }
407
- ]
408
- },
409
- actions: [
410
- {
411
- type: 'send-transaction',
412
- name: 'main-action-after-skipped-setup',
413
- arguments: {
414
- to: TEST_ADDRESSES.RECIPIENT_1,
415
- value: TEST_VALUES.ONE_ETH,
416
- data: '0x'
417
- }
418
- }
419
- ]
420
- }
421
- templates.set('skippable-setup-template', template)
422
-
423
- const callingAction: JobAction = {
424
- name: 'test-call',
425
- template: 'skippable-setup-template',
426
- arguments: {}
427
- }
428
-
429
- await (engine as any).executeTemplate(callingAction, 'skippable-setup-template', context)
430
-
431
- // Setup action should have been skipped due to setup skip condition
432
- expect(() => context.getOutput('setup-action.hash')).toThrow()
433
- // Main action should still have executed (setup skip conditions don't affect main actions)
434
- expect(context.getOutput('main-action-after-skipped-setup.hash')).toBeDefined()
435
- })
436
-
437
- it('should pass arguments to template and resolve outputs', async () => {
438
- const template: Template = {
439
- name: 'parameterized-template',
440
- actions: [
441
- {
442
- type: 'send-transaction',
443
- name: 'param-action',
444
- arguments: {
445
- to: '{{target_address}}',
446
- value: '{{amount}}',
447
- data: '0x'
448
- }
449
- }
450
- ],
451
- outputs: {
452
- transaction_hash: '{{param-action.hash}}',
453
- doubled_amount: { type: 'basic-arithmetic', arguments: { operation: 'mul', values: ['{{amount}}', 2] } }
454
- }
455
- }
456
- templates.set('parameterized-template', template)
457
-
458
- const callingAction: JobAction = {
459
- name: 'test-param-call',
460
- template: 'parameterized-template',
461
- arguments: {
462
- target_address: TEST_ADDRESSES.RECIPIENT_1,
463
- amount: TEST_VALUES.ONE_ETH
464
- }
465
- }
466
-
467
- await (engine as any).executeTemplate(callingAction, 'parameterized-template', context)
468
-
469
- // Check that outputs were stored with the calling action name
470
- expect(context.getOutput('test-param-call.transaction_hash')).toBeDefined()
471
- expect(context.getOutput('test-param-call.doubled_amount')).toBe('2000000000000000000')
472
- })
473
-
474
- it('should allow job action custom output map to override template outputs', async () => {
475
- // Mock executeAction so the inner action sets expected outputs
476
- const originalExecuteAction = (engine as any).executeAction
477
- ;(engine as any).executeAction = async function(action: JobAction | Action, ctx: ExecutionContext, scope: any) {
478
- const actionName = 'name' in action ? action.name : action.type
479
- if (actionName === 'param-action') {
480
- ctx.setOutput('param-action.hash', '0xhash123')
481
- ctx.setOutput('param-action.receipt', { status: 1, blockNumber: 111 })
482
- }
483
- }
484
-
485
- const template: Template = {
486
- name: 'tpl-custom-output',
487
- actions: [
488
- {
489
- type: 'send-transaction',
490
- name: 'param-action',
491
- arguments: {
492
- to: TEST_ADDRESSES.RECIPIENT_1,
493
- value: TEST_VALUES.ONE_ETH,
494
- data: '0x'
495
- }
496
- }
497
- ],
498
- outputs: {
499
- transaction_hash: '{{param-action.hash}}',
500
- receipt_block: '{{param-action.receipt.blockNumber}}'
501
- }
502
- }
503
- templates.set('tpl-custom-output', template)
504
-
505
- const callingAction: JobAction = {
506
- name: 'custom-call',
507
- template: 'tpl-custom-output',
508
- arguments: {},
509
- // Custom output overrides template outputs
510
- output: {
511
- myHash: '{{param-action.hash}}',
512
- staticValue: '42'
513
- } as any
514
- }
515
-
516
- await (engine as any).executeTemplate(callingAction, 'tpl-custom-output', context)
517
-
518
- // Restore original method
519
- ;(engine as any).executeAction = originalExecuteAction
520
-
521
- // Expect only custom outputs to be present for action name "custom-call"
522
- expect(context.getOutput('custom-call.myHash')).toBe('0xhash123')
523
- expect(context.getOutput('custom-call.staticValue')).toBe('42')
524
-
525
- // Template outputs should NOT be set since custom output overrides them
526
- expect(() => context.getOutput('custom-call.transaction_hash')).toThrow()
527
- expect(() => context.getOutput('custom-call.receipt_block')).toThrow()
528
- })
529
-
530
- it('should throw when template is not found', async () => {
531
- const callingAction: JobAction = {
532
- name: 'test-call',
533
- template: 'non-existent-template',
534
- arguments: {}
535
- }
536
-
537
- await expect((engine as any).executeTemplate(callingAction, 'non-existent-template', context))
538
- .rejects.toThrow('Template "non-existent-template" not found')
539
- })
540
- })
541
-
542
- describe('executePrimitive', () => {
543
- describe('send-transaction', () => {
544
- it('should send a transaction successfully', async () => {
545
- const action: Action = {
546
- type: 'send-transaction',
547
- name: 'test-tx',
548
- arguments: {
549
- to: TEST_ADDRESSES.RECIPIENT_1,
550
- value: TEST_VALUES.ONE_ETH,
551
- data: '0x'
552
- }
553
- }
554
-
555
- await (engine as any).executePrimitive(action, context, new Map())
556
-
557
- const hash = context.getOutput('test-tx.hash')
558
- const receipt = context.getOutput('test-tx.receipt')
559
-
560
- expect(hash).toBeDefined()
561
- expect(receipt).toBeDefined()
562
- expect(receipt.status).toBe(1)
563
- })
564
-
565
- it('should send transaction with resolved arguments', async () => {
566
- context.setOutput('recipient', TEST_ADDRESSES.RECIPIENT_1)
567
- context.setOutput('amount', TEST_VALUES.ONE_ETH)
568
-
569
- const action: Action = {
570
- type: 'send-transaction',
571
- name: 'resolved-tx',
572
- arguments: {
573
- to: '{{recipient}}',
574
- value: '{{amount}}',
575
- data: '0x1234'
576
- }
577
- }
578
-
579
- await (engine as any).executePrimitive(action, context, new Map())
580
-
581
- expect(context.getOutput('resolved-tx.hash')).toBeDefined()
582
- })
583
-
584
- it('should handle transaction without value and data', async () => {
585
- const action: Action = {
586
- type: 'send-transaction',
587
- name: 'minimal-tx',
588
- arguments: {
589
- to: TEST_ADDRESSES.RECIPIENT_1
590
- }
591
- }
592
-
593
- await (engine as any).executePrimitive(action, context, new Map())
594
-
595
- expect(context.getOutput('minimal-tx.hash')).toBeDefined()
596
- })
597
-
598
- it('should not store outputs when action has no name', async () => {
599
- const action: Action = {
600
- type: 'send-transaction',
601
- arguments: {
602
- to: TEST_ADDRESSES.RECIPIENT_1,
603
- value: TEST_VALUES.ONE_ETH,
604
- data: '0x'
605
- }
606
- }
607
-
608
- await (engine as any).executePrimitive(action, context, new Map())
609
-
610
- // Since no name, no outputs should be stored
611
- const outputs = (context as any).outputs
612
- expect(outputs.size).toBe(0)
613
- })
614
-
615
- it('should throw on invalid address', async () => {
616
- const action: Action = {
617
- type: 'send-transaction',
618
- arguments: {
619
- to: 'invalid-address',
620
- value: '1000000000000000000',
621
- data: '0x'
622
- }
623
- }
624
-
625
- await expect((engine as any).executePrimitive(action, context, new Map()))
626
- .rejects.toThrow()
627
- })
628
-
629
- it('should apply gas multiplier when network gasLimit is set', async () => {
630
- // Mock the network to have a gasLimit
631
- const mockNetwork = { gasLimit: 100000 }
632
- jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
633
-
634
- const action: Action = {
635
- type: 'send-transaction',
636
- name: 'gas-multiplier-tx',
637
- arguments: {
638
- to: TEST_ADDRESSES.RECIPIENT_1,
639
- value: TEST_VALUES.ONE_ETH,
640
- gasMultiplier: 1.5
641
- }
642
- }
643
-
644
- const mockSendTransaction = jest.fn().mockResolvedValue({
645
- hash: '0x123',
646
- wait: jest.fn().mockResolvedValue({ status: 1, blockNumber: 123 })
647
- })
648
- const resolvedSigner = await context.getResolvedSigner()
649
- jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
650
-
651
- await (engine as any).executePrimitive(action, context, new Map())
652
-
653
- expect(mockSendTransaction).toHaveBeenCalledWith(
654
- expect.objectContaining({
655
- gasLimit: 150000 // 100000 * 1.5
656
- })
657
- )
658
- })
659
-
660
- it('should estimate gas and apply multiplier when no network gasLimit is set', async () => {
661
- // Mock the network to have no gasLimit
662
- const mockNetwork = {}
663
- jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
664
-
665
- const resolvedSigner = await context.getResolvedSigner()
666
- const mockEstimateGas = jest.fn().mockResolvedValue(BigInt(80000))
667
- jest.spyOn(resolvedSigner, 'estimateGas').mockImplementation(mockEstimateGas)
668
-
669
- const mockSendTransaction = jest.fn().mockResolvedValue({
670
- hash: '0x123',
671
- wait: jest.fn().mockResolvedValue({ status: 1, blockNumber: 123 })
672
- })
673
- jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
674
-
675
- const action: Action = {
676
- type: 'send-transaction',
677
- name: 'gas-estimate-tx',
678
- arguments: {
679
- to: TEST_ADDRESSES.RECIPIENT_1,
680
- gasMultiplier: 2.0
681
- }
682
- }
683
-
684
- await (engine as any).executePrimitive(action, context, new Map())
685
-
686
- expect(mockEstimateGas).toHaveBeenCalledWith(
687
- expect.objectContaining({
688
- to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
689
- })
690
- )
691
- expect(mockSendTransaction).toHaveBeenCalledWith(
692
- expect.objectContaining({
693
- gasLimit: 160000 // 80000 * 2.0
694
- })
695
- )
696
- })
697
-
698
- it('should work with resolved gasMultiplier value', async () => {
699
- context.setOutput('multiplier', 1.25)
700
- const mockNetwork = { gasLimit: 100000 }
701
- jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
702
-
703
- const mockSendTransaction = jest.fn().mockResolvedValue({
704
- hash: '0x123',
705
- wait: jest.fn().mockResolvedValue({ status: 1, blockNumber: 123 })
706
- })
707
- const resolvedSigner = await context.getResolvedSigner()
708
- jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
709
-
710
- const action: Action = {
711
- type: 'send-transaction',
712
- name: 'resolved-multiplier-tx',
713
- arguments: {
714
- to: TEST_ADDRESSES.RECIPIENT_1,
715
- gasMultiplier: '{{multiplier}}'
716
- }
717
- }
718
-
719
- await (engine as any).executePrimitive(action, context, new Map())
720
-
721
- expect(mockSendTransaction).toHaveBeenCalledWith(
722
- expect.objectContaining({
723
- gasLimit: 125000 // 100000 * 1.25
724
- })
725
- )
726
- })
727
-
728
- it('should throw error for invalid gasMultiplier', async () => {
729
- const action: Action = {
730
- type: 'send-transaction',
731
- arguments: {
732
- to: TEST_ADDRESSES.RECIPIENT_1,
733
- gasMultiplier: -1.0
734
- }
735
- }
736
-
737
- await expect((engine as any).executePrimitive(action, context, new Map()))
738
- .rejects.toThrow('gasMultiplier must be a positive number')
739
- })
740
-
741
- it('should throw error for zero gasMultiplier', async () => {
742
- const action: Action = {
743
- type: 'send-transaction',
744
- arguments: {
745
- to: TEST_ADDRESSES.RECIPIENT_1,
746
- gasMultiplier: 0
747
- }
748
- }
749
-
750
- await expect((engine as any).executePrimitive(action, context, new Map()))
751
- .rejects.toThrow('gasMultiplier must be a positive number')
752
- })
753
- })
754
-
755
- describe('create-contract', () => {
756
- it('should create a contract successfully', async () => {
757
- const action: Action = {
758
- type: 'create-contract',
759
- name: 'test-contract',
760
- arguments: {
761
- data: TEST_BYTECODES.SIMPLE_CONTRACT
762
- }
763
- }
764
-
765
- await (engine as any).executePrimitive(action, context, new Map())
766
-
767
- const hash = context.getOutput('test-contract.hash')
768
- const receipt = context.getOutput('test-contract.receipt')
769
- const address = context.getOutput('test-contract.address')
770
-
771
- expect(hash).toBeDefined()
772
- expect(receipt).toBeDefined()
773
- expect(address).toBeDefined()
774
- expect(receipt.status).toBe(1)
775
- expect(receipt.contractAddress).toBe(address)
776
- })
777
-
778
- it('should create contract with resolved arguments', async () => {
779
- context.setOutput('contract_bytecode', TEST_BYTECODES.SIMPLE_CONTRACT)
780
-
781
- const action: Action = {
782
- type: 'create-contract',
783
- name: 'resolved-contract',
784
- arguments: {
785
- data: '{{contract_bytecode}}'
786
- // Removed value parameter to avoid payable constructor issues
787
- }
788
- }
789
-
790
- await (engine as any).executePrimitive(action, context, new Map())
791
-
792
- expect(context.getOutput('resolved-contract.address')).toBeDefined()
793
- })
794
-
795
- it('should apply gas multiplier when network gasLimit is set', async () => {
796
- // Mock the network to have a gasLimit
797
- const mockNetwork = { gasLimit: 200000 }
798
- jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
799
-
800
- const action: Action = {
801
- type: 'create-contract',
802
- name: 'gas-multiplier-contract',
803
- arguments: {
804
- data: TEST_BYTECODES.SIMPLE_CONTRACT,
805
- gasMultiplier: 1.5
806
- }
807
- }
808
-
809
- const mockSendTransaction = jest.fn().mockResolvedValue({
810
- hash: '0x123',
811
- wait: jest.fn().mockResolvedValue({
812
- status: 1,
813
- blockNumber: 123,
814
- contractAddress: '0x5FbDB2315678afecb367f032d93F642f64180aa3'
815
- })
816
- })
817
- const resolvedSigner = await context.getResolvedSigner()
818
- jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
819
-
820
- await (engine as any).executePrimitive(action, context, new Map())
821
-
822
- expect(mockSendTransaction).toHaveBeenCalledWith(
823
- expect.objectContaining({
824
- to: null, // Contract creation
825
- gasLimit: 300000 // 200000 * 1.5
826
- })
827
- )
828
- })
829
-
830
- it('should estimate gas and apply multiplier when no network gasLimit is set', async () => {
831
- // Mock the network to have no gasLimit
832
- const mockNetwork = {}
833
- jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
834
-
835
- const resolvedSigner = await context.getResolvedSigner()
836
- const mockEstimateGas = jest.fn().mockResolvedValue(BigInt(150000))
837
- jest.spyOn(resolvedSigner, 'estimateGas').mockImplementation(mockEstimateGas)
838
-
839
- const mockSendTransaction = jest.fn().mockResolvedValue({
840
- hash: '0x123',
841
- wait: jest.fn().mockResolvedValue({
842
- status: 1,
843
- blockNumber: 123,
844
- contractAddress: '0x5FbDB2315678afecb367f032d93F642f64180aa3'
845
- })
846
- })
847
- jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
848
-
849
- const action: Action = {
850
- type: 'create-contract',
851
- name: 'gas-estimate-contract',
852
- arguments: {
853
- data: TEST_BYTECODES.SIMPLE_CONTRACT,
854
- gasMultiplier: 2.0
855
- }
856
- }
857
-
858
- await (engine as any).executePrimitive(action, context, new Map())
859
-
860
- expect(mockEstimateGas).toHaveBeenCalledWith(
861
- expect.objectContaining({
862
- to: null,
863
- data: TEST_BYTECODES.SIMPLE_CONTRACT
864
- })
865
- )
866
- expect(mockSendTransaction).toHaveBeenCalledWith(
867
- expect.objectContaining({
868
- gasLimit: 300000 // 150000 * 2.0
869
- })
870
- )
871
- })
872
-
873
- it('should work with resolved gasMultiplier value', async () => {
874
- context.setOutput('multiplier', 1.25)
875
- const mockNetwork = { gasLimit: 200000 }
876
- jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
877
-
878
- const mockSendTransaction = jest.fn().mockResolvedValue({
879
- hash: '0x123',
880
- wait: jest.fn().mockResolvedValue({
881
- status: 1,
882
- blockNumber: 123,
883
- contractAddress: '0x5FbDB2315678afecb367f032d93F642f64180aa3'
884
- })
885
- })
886
- const resolvedSigner = await context.getResolvedSigner()
887
- jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
888
-
889
- const action: Action = {
890
- type: 'create-contract',
891
- name: 'resolved-multiplier-contract',
892
- arguments: {
893
- data: TEST_BYTECODES.SIMPLE_CONTRACT,
894
- gasMultiplier: '{{multiplier}}'
895
- }
896
- }
897
-
898
- await (engine as any).executePrimitive(action, context, new Map())
899
-
900
- expect(mockSendTransaction).toHaveBeenCalledWith(
901
- expect.objectContaining({
902
- gasLimit: 250000 // 200000 * 1.25
903
- })
904
- )
905
- })
906
-
907
- it('should throw error for invalid gasMultiplier', async () => {
908
- const action: Action = {
909
- type: 'create-contract',
910
- arguments: {
911
- data: TEST_BYTECODES.SIMPLE_CONTRACT,
912
- gasMultiplier: -1.0
913
- }
914
- }
915
-
916
- await expect((engine as any).executePrimitive(action, context, new Map()))
917
- .rejects.toThrow('gasMultiplier must be a positive number')
918
- })
919
-
920
- it('should throw error for zero gasMultiplier', async () => {
921
- const action: Action = {
922
- type: 'create-contract',
923
- arguments: {
924
- data: TEST_BYTECODES.SIMPLE_CONTRACT,
925
- gasMultiplier: 0
926
- }
927
- }
928
-
929
- await expect((engine as any).executePrimitive(action, context, new Map()))
930
- .rejects.toThrow('gasMultiplier must be a positive number')
931
- })
932
-
933
- it('should handle contract creation without value and gasMultiplier', async () => {
934
- const action: Action = {
935
- type: 'create-contract',
936
- name: 'minimal-contract',
937
- arguments: {
938
- data: TEST_BYTECODES.SIMPLE_CONTRACT
939
- }
940
- }
941
-
942
- await (engine as any).executePrimitive(action, context, new Map())
943
-
944
- expect(context.getOutput('minimal-contract.address')).toBeDefined()
945
- })
946
-
947
- it('should not store outputs when action has no name', async () => {
948
- const action: Action = {
949
- type: 'create-contract',
950
- arguments: {
951
- data: TEST_BYTECODES.SIMPLE_CONTRACT
952
- }
953
- }
954
-
955
- const outputsBefore = (context as any).outputs.size
956
-
957
- await (engine as any).executePrimitive(action, context, new Map())
958
-
959
- // Since no name, no outputs should be stored
960
- const outputsAfter = (context as any).outputs.size
961
- expect(outputsAfter).toBe(outputsBefore)
962
- })
963
-
964
- it('should throw on invalid bytecode', async () => {
965
- const action: Action = {
966
- type: 'create-contract',
967
- arguments: {
968
- data: 'invalid-bytecode'
969
- }
970
- }
971
-
972
- await expect((engine as any).executePrimitive(action, context, new Map()))
973
- .rejects.toThrow()
974
- })
975
- })
976
-
977
- describe('send-signed-transaction', () => {
978
- it('should broadcast a signed transaction', async () => {
979
- // Create a signed transaction using the same private key
980
- const wallet = new ethers.Wallet(TEST_PRIVATE_KEY, anvilProvider)
981
-
982
- const tx = await wallet.populateTransaction({
983
- to: TEST_ADDRESSES.RECIPIENT_1,
984
- value: ethers.parseEther('1'),
985
- gasLimit: 21000
986
- })
987
-
988
- const signedTx = await wallet.signTransaction(tx)
989
-
990
- const action: Action = {
991
- type: 'send-signed-transaction',
992
- name: 'signed-tx',
993
- arguments: {
994
- transaction: signedTx
995
- }
996
- }
997
-
998
- await (engine as any).executePrimitive(action, context, new Map())
999
-
1000
- expect(context.getOutput('signed-tx.hash')).toBeDefined()
1001
- expect(context.getOutput('signed-tx.receipt')).toBeDefined()
1002
- })
1003
-
1004
- it('should resolve transaction from context', async () => {
1005
- const wallet = new ethers.Wallet(TEST_PRIVATE_KEY, anvilProvider)
1006
-
1007
- const tx = await wallet.populateTransaction({
1008
- to: TEST_ADDRESSES.RECIPIENT_1,
1009
- value: ethers.parseEther('1'),
1010
- gasLimit: 21000
1011
- })
1012
-
1013
- const signedTx = await wallet.signTransaction(tx)
1014
- context.setOutput('prepared_tx', signedTx)
1015
-
1016
- const action: Action = {
1017
- type: 'send-signed-transaction',
1018
- name: 'resolved-signed-tx',
1019
- arguments: {
1020
- transaction: '{{prepared_tx}}'
1021
- }
1022
- }
1023
-
1024
- await (engine as any).executePrimitive(action, context, new Map())
1025
-
1026
- expect(context.getOutput('resolved-signed-tx.hash')).toBeDefined()
1027
- })
1028
- })
1029
-
1030
- describe('static', () => {
1031
- it('should return the provided value unchanged', async () => {
1032
- const action: Action = {
1033
- type: 'static',
1034
- name: 'test-static',
1035
- arguments: {
1036
- value: 'hello world'
1037
- }
1038
- }
1039
-
1040
- await (engine as any).executePrimitive(action, context, new Map())
1041
-
1042
- expect(context.getOutput('test-static.value')).toBe('hello world')
1043
- })
1044
-
1045
- it('should resolve and return complex values', async () => {
1046
- context.setOutput('input_value', { foo: 'bar', number: 42 })
1047
-
1048
- const action: Action = {
1049
- type: 'static',
1050
- name: 'complex-static',
1051
- arguments: {
1052
- value: '{{input_value}}'
1053
- }
1054
- }
1055
-
1056
- await (engine as any).executePrimitive(action, context, new Map())
1057
-
1058
- expect(context.getOutput('complex-static.value')).toEqual({ foo: 'bar', number: 42 })
1059
- })
1060
-
1061
- it('should work with numeric values', async () => {
1062
- const action: Action = {
1063
- type: 'static',
1064
- name: 'numeric-static',
1065
- arguments: {
1066
- value: 12345
1067
- }
1068
- }
1069
-
1070
- await (engine as any).executePrimitive(action, context, new Map())
1071
-
1072
- expect(context.getOutput('numeric-static.value')).toBe(12345)
1073
- })
1074
-
1075
- it('should work with boolean values', async () => {
1076
- const action: Action = {
1077
- type: 'static',
1078
- name: 'boolean-static',
1079
- arguments: {
1080
- value: true
1081
- }
1082
- }
1083
-
1084
- await (engine as any).executePrimitive(action, context, new Map())
1085
-
1086
- expect(context.getOutput('boolean-static.value')).toBe(true)
1087
- })
1088
-
1089
- it('should work with array values', async () => {
1090
- const testArray = [1, 2, 3, 'test']
1091
- const action: Action = {
1092
- type: 'static',
1093
- name: 'array-static',
1094
- arguments: {
1095
- value: testArray
1096
- }
1097
- }
1098
-
1099
- await (engine as any).executePrimitive(action, context, new Map())
1100
-
1101
- expect(context.getOutput('array-static.value')).toEqual(testArray)
1102
- })
1103
-
1104
- it('should not store outputs when action has no name', async () => {
1105
- const action: Action = {
1106
- type: 'static',
1107
- arguments: {
1108
- value: 'test value'
1109
- }
1110
- }
1111
-
1112
- const outputsBefore = Object.keys((context as any).outputs || {}).length
1113
-
1114
- await (engine as any).executePrimitive(action, context, new Map())
1115
-
1116
- const outputsAfter = Object.keys((context as any).outputs || {}).length
1117
- expect(outputsAfter).toBe(outputsBefore)
1118
- })
1119
-
1120
- it('should resolve template variables in scope', async () => {
1121
- const scope = new Map()
1122
- scope.set('template_var', 'resolved from scope')
1123
-
1124
- const action: Action = {
1125
- type: 'static',
1126
- name: 'scope-static',
1127
- arguments: {
1128
- value: '{{template_var}}'
1129
- }
1130
- }
1131
-
1132
- await (engine as any).executePrimitive(action, context, scope)
1133
-
1134
- expect(context.getOutput('scope-static.value')).toBe('resolved from scope')
1135
- })
1136
- })
1137
-
1138
- it('should throw on unknown primitive action type', async () => {
1139
- const action: any = {
1140
- type: 'unknown-action',
1141
- name: 'unknown',
1142
- arguments: {}
1143
- }
1144
-
1145
- await expect((engine as any).executePrimitive(action, context, new Map()))
1146
- .rejects.toThrow('Unknown or unimplemented primitive action type: unknown-action')
1147
- })
1148
-
1149
- it('should handle custom outputs for primitive actions', async () => {
1150
- const action: JobAction = {
1151
- name: 'custom-primitive',
1152
- type: 'send-transaction',
1153
- arguments: {
1154
- to: TEST_ADDRESSES.RECIPIENT_1,
1155
- value: TEST_VALUES.ONE_ETH,
1156
- data: '0x'
1157
- },
1158
- output: {
1159
- value: '0xDE280948Af8A9762B6984995C8c3c7F5AEB921Bf'
1160
- } as any
1161
- }
1162
-
1163
- await (engine as any).executeAction(action, context, new Map())
1164
-
1165
- // Should have the custom static output, not the default transaction outputs
1166
- expect(context.getOutput('custom-primitive.value')).toBe('0xDE280948Af8A9762B6984995C8c3c7F5AEB921Bf')
1167
-
1168
- // Default outputs should not be present when custom output is specified
1169
- expect(() => context.getOutput('custom-primitive.hash')).toThrow()
1170
- expect(() => context.getOutput('custom-primitive.receipt')).toThrow()
1171
- })
1172
- })
1173
-
1174
- describe('evaluateSkipConditions', () => {
1175
- it('should return false for undefined conditions', async () => {
1176
- const result = await (engine as any).evaluateSkipConditions(undefined, context, new Map())
1177
- expect(result).toBe(false)
1178
- })
1179
-
1180
- it('should return false for empty conditions array', async () => {
1181
- const result = await (engine as any).evaluateSkipConditions([], context, new Map())
1182
- expect(result).toBe(false)
1183
- })
1184
-
1185
- it('should return true if any condition is met', async () => {
1186
- context.setOutput('flag1', 0)
1187
- context.setOutput('flag2', 1)
1188
-
1189
- const conditions = [
1190
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{flag1}}', 1] } },
1191
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{flag2}}', 1] } }
1192
- ]
1193
-
1194
- const result = await (engine as any).evaluateSkipConditions(conditions, context, new Map())
1195
- expect(result).toBe(true)
1196
- })
1197
-
1198
- it('should return false if no conditions are met', async () => {
1199
- context.setOutput('flag1', 0)
1200
- context.setOutput('flag2', 0)
1201
-
1202
- const conditions = [
1203
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{flag1}}', 1] } },
1204
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{flag2}}', 1] } }
1205
- ]
1206
-
1207
- const result = await (engine as any).evaluateSkipConditions(conditions, context, new Map())
1208
- expect(result).toBe(false)
1209
- })
1210
-
1211
- it('should return true when a value-empty condition resolves to true', async () => {
1212
- context.setOutput('payloadMap', { 10: '0xdeadbeef' })
1213
- const scope = new Map<string, any>([['missingChainId', 999]])
1214
-
1215
- const conditions = [
1216
- {
1217
- type: 'value-empty',
1218
- arguments: {
1219
- value: {
1220
- type: 'read-json',
1221
- arguments: {
1222
- json: '{{payloadMap}}',
1223
- path: '{{missingChainId}}'
1224
- }
1225
- }
1226
- }
1227
- }
1228
- ]
1229
-
1230
- const result = await (engine as any).evaluateSkipConditions(conditions, context, scope)
1231
- expect(result).toBe(true)
1232
- })
1233
- })
1234
-
1235
- describe('topologicalSortActions', () => {
1236
- it('should sort actions with no dependencies', async () => {
1237
- const job: Job = {
1238
- name: 'no-deps-job',
1239
- version: '1.0.0',
1240
- actions: [
1241
- { name: 'action-c', template: 'send-transaction', arguments: {} },
1242
- { name: 'action-a', template: 'send-transaction', arguments: {} },
1243
- { name: 'action-b', template: 'send-transaction', arguments: {} }
1244
- ]
1245
- }
1246
-
1247
- const result = (engine as any).topologicalSortActions(job)
1248
- expect(result).toEqual(['action-c', 'action-a', 'action-b'])
1249
- })
1250
-
1251
- it('should sort actions with dependencies correctly', async () => {
1252
- const job: Job = {
1253
- name: 'deps-job',
1254
- version: '1.0.0',
1255
- actions: [
1256
- { name: 'action-c', template: 'send-transaction', arguments: {}, depends_on: ['action-a', 'action-b'] },
1257
- { name: 'action-b', template: 'send-transaction', arguments: {}, depends_on: ['action-a'] },
1258
- { name: 'action-a', template: 'send-transaction', arguments: {} }
1259
- ]
1260
- }
1261
-
1262
- const result = (engine as any).topologicalSortActions(job)
1263
- expect(result).toEqual(['action-a', 'action-b', 'action-c'])
1264
- })
1265
-
1266
- it('should throw on circular dependencies', async () => {
1267
- const job: Job = {
1268
- name: 'circular-job',
1269
- version: '1.0.0',
1270
- actions: [
1271
- { name: 'action-a', template: 'send-transaction', arguments: {}, depends_on: ['action-b'] },
1272
- { name: 'action-b', template: 'send-transaction', arguments: {}, depends_on: ['action-a'] }
1273
- ]
1274
- }
1275
-
1276
- expect(() => (engine as any).topologicalSortActions(job))
1277
- .toThrow('Circular dependency detected among actions in job "circular-job".')
1278
- })
1279
-
1280
- it('should throw on invalid dependency', async () => {
1281
- const job: Job = {
1282
- name: 'invalid-dep-job',
1283
- version: '1.0.0',
1284
- actions: [
1285
- { name: 'action-a', template: 'send-transaction', arguments: {}, depends_on: ['non-existent'] }
1286
- ]
1287
- }
1288
-
1289
- expect(() => (engine as any).topologicalSortActions(job))
1290
- .toThrow('Action "action-a" in job "invalid-dep-job" has an invalid dependency on "non-existent", which does not exist.')
1291
- })
1292
- })
1293
-
1294
- describe('integration tests', () => {
1295
- it('should execute a complex job with templates, dependencies, and skip conditions', async () => {
1296
- // Mock executeAction to track execution and avoid transaction issues
1297
- const executedActions: string[] = []
1298
- const originalExecuteAction = (engine as any).executeAction
1299
- ;(engine as any).executeAction = async function(action: JobAction | Action, ctx: ExecutionContext, scope: any) {
1300
- const actionName = 'name' in action ? action.name : action.type
1301
- if (actionName) {
1302
- executedActions.push(actionName)
1303
- // Mock successful execution by setting outputs for template calls
1304
- if (actionName === 'setup-step') {
1305
- // Mock the template execution outputs and template outputs
1306
- ctx.setOutput('fund-contract.hash', 'mock-fund-hash')
1307
- ctx.setOutput('fund-contract.receipt', { status: 1, blockNumber: 200 })
1308
- // Set template outputs (these are what the test expects)
1309
- ctx.setOutput('setup-step.funded_address', '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC')
1310
- ctx.setOutput('setup-step.fund_amount', '2000000000000000000')
1311
- } else if (actionName === 'deploy-step') {
1312
- ctx.setOutput('deploy-contract.hash', 'mock-deploy-hash')
1313
- ctx.setOutput('deploy-contract.receipt', { status: 1, blockNumber: 201 })
1314
- // Set template outputs
1315
- ctx.setOutput('deploy-step.deployment_hash', 'mock-deploy-hash')
1316
- } else if (actionName === 'final-check') {
1317
- ctx.setOutput('final-check.hash', 'mock-final-hash')
1318
- ctx.setOutput('final-check.receipt', { status: 1, blockNumber: 202 })
1319
- }
1320
- }
1321
- }
1322
-
1323
- // Create a template that sets up some state
1324
- const setupTemplate: Template = {
1325
- name: 'setup-template',
1326
- actions: [
1327
- {
1328
- type: 'send-transaction',
1329
- name: 'fund-contract',
1330
- arguments: {
1331
- to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
1332
- value: '2000000000000000000',
1333
- data: '0x'
1334
- }
1335
- }
1336
- ],
1337
- outputs: {
1338
- funded_address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
1339
- fund_amount: '2000000000000000000'
1340
- }
1341
- }
1342
-
1343
- // Create a template that uses the setup output
1344
- const deployTemplate: Template = {
1345
- name: 'deploy-template',
1346
- actions: [
1347
- {
1348
- type: 'send-transaction',
1349
- name: 'deploy-contract',
1350
- arguments: {
1351
- to: '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65',
1352
- value: '1000000000000000000',
1353
- data: '0x608060405234801561000f575f5ffd5b50603e80601c5f395ff3fe60806040525f80fdfea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033'
1354
- }
1355
- }
1356
- ],
1357
- outputs: {
1358
- deployment_hash: '{{deploy-contract.hash}}'
1359
- }
1360
- }
1361
-
1362
- templates.set('setup-template', setupTemplate)
1363
- templates.set('deploy-template', deployTemplate)
1364
-
1365
- const complexJob: Job = {
1366
- name: 'complex-job',
1367
- version: '1.0.0',
1368
- actions: [
1369
- {
1370
- name: 'deploy-step',
1371
- template: 'deploy-template',
1372
- arguments: {
1373
- funded_address: '{{setup-step.funded_address}}',
1374
- fund_amount: '{{setup-step.fund_amount}}'
1375
- },
1376
- depends_on: ['setup-step']
1377
- },
1378
- {
1379
- name: 'setup-step',
1380
- template: 'setup-template',
1381
- arguments: {}
1382
- },
1383
- {
1384
- name: 'final-check',
1385
- template: 'send-transaction',
1386
- arguments: {
1387
- to: '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc',
1388
- value: '1000000000000000000',
1389
- data: '0x'
1390
- },
1391
- depends_on: ['deploy-step']
1392
- }
1393
- ]
1394
- }
1395
-
1396
- await engine.executeJob(complexJob, context)
1397
-
1398
- // Restore original method
1399
- ;(engine as any).executeAction = originalExecuteAction
1400
-
1401
- // Verify execution order respects dependencies
1402
- expect(executedActions).toEqual(['setup-step', 'deploy-step', 'final-check'])
1403
-
1404
- // Verify all the expected outputs exist
1405
- expect(context.getOutput('setup-step.funded_address')).toBe('0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC')
1406
- expect(context.getOutput('setup-step.fund_amount')).toBe('2000000000000000000')
1407
- expect(context.getOutput('deploy-step.deployment_hash')).toBe('mock-deploy-hash')
1408
- expect(context.getOutput('final-check.hash')).toBe('mock-final-hash')
1409
- })
1410
-
1411
- it('should handle failed transactions appropriately', async () => {
1412
- // Try to send to an invalid address (will cause transaction to fail at validation)
1413
- const job: Job = {
1414
- name: 'failing-job',
1415
- version: '1.0.0',
1416
- actions: [
1417
- {
1418
- name: 'failing-tx',
1419
- template: 'send-transaction',
1420
- arguments: {
1421
- to: 'not-an-address',
1422
- value: '1000000000000000000',
1423
- data: '0x'
1424
- }
1425
- }
1426
- ]
1427
- }
1428
-
1429
- await expect(engine.executeJob(job, context)).rejects.toThrow()
1430
- })
1431
- })
1432
-
1433
- describe('setup skip conditions', () => {
1434
- it('should skip setup actions when contract-exists condition is met', async () => {
1435
- // Deploy a contract first using anvil_setCode
1436
- const contractAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
1437
- const miniContractBytecode = '0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80636df5b97a14610038578063f8a8fd6d14610068575b5f5ffd5b610052600480360381019061004d91906100da565b610086565b60405161005f9190610127565b60405180910390f35b61007061009b565b60405161007d9190610127565b60405180910390f35b5f8183610093919061016d565b905092915050565b5f602a905090565b5f5ffd5b5f819050919050565b6100b9816100a7565b81146100c3575f5ffd5b50565b5f813590506100d4816100b0565b92915050565b5f5f604083850312156100f0576100ef6100a3565b5b5f6100fd858286016100c6565b925050602061010e858286016100c6565b9150509250929050565b610121816100a7565b82525050565b5f60208201905061013a5f830184610118565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610177826100a7565b9150610182836100a7565b9250828202610190816100a7565b915082820484148315176101a7576101a6610140565b5b509291505056fea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033'
1438
- await anvilProvider.send('anvil_setCode', [contractAddress, miniContractBytecode])
1439
-
1440
- // Create a template that should skip setup because the contract exists
1441
- const template: Template = {
1442
- name: 'test-contract-exists-skip',
1443
- setup: {
1444
- skip_condition: [
1445
- { type: 'contract-exists', arguments: { address: contractAddress } }
1446
- ],
1447
- actions: [
1448
- {
1449
- type: 'send-transaction',
1450
- name: 'setup-action',
1451
- arguments: {
1452
- to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
1453
- value: '1000000000000000000',
1454
- data: '0x'
1455
- }
1456
- }
1457
- ]
1458
- },
1459
- actions: [
1460
- {
1461
- type: 'send-transaction',
1462
- name: 'main-action',
1463
- arguments: {
1464
- to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
1465
- value: '500000000000000000',
1466
- data: '0x'
1467
- }
1468
- }
1469
- ]
1470
- }
1471
-
1472
- templates.set('test-contract-exists-skip', template)
1473
-
1474
- const action: JobAction = {
1475
- name: 'test-skip',
1476
- template: 'test-contract-exists-skip',
1477
- arguments: {}
1478
- }
1479
-
1480
- await (engine as any).executeAction(action, context, new Map())
1481
-
1482
- // Setup action should have been skipped, so no output
1483
- expect(() => context.getOutput('setup-action.hash')).toThrow()
1484
- // Main action should still have executed
1485
- expect(context.getOutput('main-action.hash')).toBeDefined()
1486
- })
1487
- })
1488
-
1489
- describe('test-nicks-method action', () => {
1490
- it('should successfully test Nick\'s method with a simple contract', async () => {
1491
- const action: Action = {
1492
- type: 'test-nicks-method',
1493
- name: 'nick-test',
1494
- arguments: {
1495
- bytecode: TEST_BYTECODES.SIMPLE_RETURN_42,
1496
- gasPrice: ethers.parseUnits('10', 'gwei'),
1497
- gasLimit: 100000n,
1498
- fundingAmount: ethers.parseEther('0.001')
1499
- }
1500
- }
1501
-
1502
- await (engine as any).executeAction(action, context, new Map())
1503
-
1504
- // Should have success output
1505
- expect(context.getOutput('nick-test.success')).toBe(true)
1506
- })
1507
-
1508
- it('should prevent failing Nick\'s method from being tested twice', async () => {
1509
- const action: Action = {
1510
- type: 'test-nicks-method',
1511
- name: 'nick-test-fail-twice',
1512
- arguments: {
1513
- bytecode: TEST_BYTECODES.BROKEN_BYTECODE, // Will break
1514
- }
1515
- }
1516
-
1517
- await expect((engine as any).executeAction(action, context, new Map())).rejects.toThrow(new Error(`Nick's method test failed for action "nick-test-fail-twice"`))
1518
- await expect((engine as any).executeAction(action, context, new Map())).rejects.toThrow(new Error('Nick\'s method test already failed this run'))
1519
- })
1520
-
1521
- it('should prevent passing Nick\'s method from being tested twice', async () => {
1522
- const action: Action = {
1523
- type: 'test-nicks-method',
1524
- name: 'nick-test-pass-twice',
1525
- arguments: {
1526
- bytecode: TEST_BYTECODES.SIMPLE_RETURN_42, // Will pass
1527
- }
1528
- }
1529
-
1530
- await (engine as any).executeAction(action, context, new Map())
1531
- await (engine as any).executeAction(action, context, new Map())
1532
-
1533
- // Should have success output
1534
- expect(context.getOutput('nick-test-pass-twice.success')).toBe(true)
1535
- })
1536
-
1537
- it('should handle missing optional parameters', async () => {
1538
- const action: Action = {
1539
- type: 'test-nicks-method',
1540
- name: 'nick-test-defaults',
1541
- arguments: {
1542
- bytecode: TEST_BYTECODES.SIMPLE_RETURN_42
1543
- // All other parameters should use defaults
1544
- }
1545
- }
1546
-
1547
- await (engine as any).executeAction(action, context, new Map())
1548
-
1549
- // Should have success output
1550
- expect(context.getOutput('nick-test-defaults.success')).toBe(true)
1551
- })
1552
-
1553
- it('should use default bytecode when none provided', async () => {
1554
- const action: Action = {
1555
- type: 'test-nicks-method',
1556
- name: 'nick-test-default-bytecode',
1557
- arguments: {
1558
- // No bytecode provided - should use default
1559
- }
1560
- }
1561
-
1562
- await (engine as any).executeAction(action, context, new Map())
1563
-
1564
- // Should have success output
1565
- expect(context.getOutput('nick-test-default-bytecode.success')).toBe(true)
1566
- })
1567
- })
1568
-
1569
- describe('ignore verify errors feature', () => {
1570
- beforeEach(() => {
1571
- // Mock fs.readFile to return valid build info
1572
- jest.doMock('fs/promises', () => ({
1573
- readFile: jest.fn().mockResolvedValue(JSON.stringify({
1574
- _format: 'hh-sol-build-info-1',
1575
- id: 'test-id',
1576
- solcVersion: '0.8.0',
1577
- input: {
1578
- language: 'Solidity',
1579
- sources: {
1580
- 'TestContract.sol': {
1581
- content: 'contract TestContract { }'
1582
- }
1583
- },
1584
- settings: {
1585
- optimizer: { enabled: true, runs: 200 },
1586
- outputSelection: { '*': { '*': ['*'] } }
1587
- }
1588
- },
1589
- output: {
1590
- contracts: {},
1591
- sources: {}
1592
- }
1593
- }))
1594
- }))
1595
-
1596
- // Create a mock verification registry with a failing platform
1597
- const mockVerificationRegistry = new VerificationPlatformRegistry()
1598
- const mockPlatform = {
1599
- name: 'mock-platform',
1600
- supportsNetwork: jest.fn().mockReturnValue(true),
1601
- isConfigured: jest.fn().mockReturnValue(true),
1602
- getConfigurationRequirements: jest.fn().mockReturnValue(''),
1603
- isContractAlreadyVerified: jest.fn().mockResolvedValue(false),
1604
- verifyContract: jest.fn().mockRejectedValue(new Error('Verification failed'))
1605
- }
1606
- mockVerificationRegistry.register(mockPlatform)
1607
-
1608
- engine = new ExecutionEngine(templates, {
1609
- verificationRegistry: mockVerificationRegistry,
1610
- ignoreVerifyErrors: true
1611
- })
1612
- })
1613
-
1614
- it('should collect verification warnings when ignoreVerifyErrors is enabled', async () => {
1615
- const mockContract = {
1616
- sourceName: 'TestContract.sol',
1617
- contractName: 'TestContract',
1618
- compiler: { version: '0.8.0' },
1619
- buildInfoId: 'test-build-id',
1620
- source: 'contract TestContract { }',
1621
- creationCode: '0x608060405234801561000f575f5ffd5b50602a5f526020601ff3',
1622
- abi: [],
1623
- _sources: new Set(['TestContract.sol', '/path/to/build-info/test.json'])
1624
- }
1625
-
1626
- context.contractRepository.addForTesting({
1627
- contractName: mockContract.contractName,
1628
- abi: mockContract.abi,
1629
- bytecode: mockContract.creationCode,
1630
- sourceName: mockContract.sourceName,
1631
- source: mockContract.source,
1632
- compiler: mockContract.compiler,
1633
- buildInfoId: mockContract.buildInfoId,
1634
- _path: '/test/path',
1635
- _hash: 'test-hash'
1636
- })
1637
-
1638
- const action: Action = {
1639
- type: 'verify-contract',
1640
- name: 'test-verify',
1641
- arguments: {
1642
- address: TEST_ADDRESSES.RECIPIENT_1,
1643
- contract: '{{Contract(TestContract)}}',
1644
- platform: 'mock-platform'
1645
- }
1646
- }
1647
-
1648
- // Should not throw even though verification fails
1649
- await expect((engine as any).executePrimitive(action, context, new Map()))
1650
- .resolves.not.toThrow()
1651
-
1652
- // Should have collected the warning
1653
- const warnings = engine.getVerificationWarnings()
1654
- expect(warnings).toHaveLength(1)
1655
- expect(warnings[0]).toMatchObject({
1656
- actionName: 'test-verify',
1657
- address: TEST_ADDRESSES.RECIPIENT_1,
1658
- contractName: 'TestContract.sol:TestContract',
1659
- platform: 'mock-platform',
1660
- error: 'Action "test-verify": No build-info file found in contract sources'
1661
- })
1662
- })
1663
-
1664
- it('should throw verification errors when ignoreVerifyErrors is disabled', async () => {
1665
- // Create engine with ignoreVerifyErrors disabled
1666
- const mockVerificationRegistry = new VerificationPlatformRegistry()
1667
- const mockPlatform = {
1668
- name: 'mock-platform',
1669
- supportsNetwork: jest.fn().mockReturnValue(true),
1670
- isConfigured: jest.fn().mockReturnValue(true),
1671
- getConfigurationRequirements: jest.fn().mockReturnValue(''),
1672
- isContractAlreadyVerified: jest.fn().mockResolvedValue(false),
1673
- verifyContract: jest.fn().mockRejectedValue(new Error('Verification failed'))
1674
- }
1675
- mockVerificationRegistry.register(mockPlatform)
1676
-
1677
- const engineWithoutIgnore = new ExecutionEngine(templates, {
1678
- verificationRegistry: mockVerificationRegistry,
1679
- ignoreVerifyErrors: false
1680
- })
1681
-
1682
- const mockContract = {
1683
- sourceName: 'TestContract.sol',
1684
- contractName: 'TestContract',
1685
- compiler: { version: '0.8.0' },
1686
- buildInfoId: 'test-build-id',
1687
- source: 'contract TestContract { }',
1688
- creationCode: '0x608060405234801561000f575f5ffd5b50602a5f526020601ff3',
1689
- abi: [],
1690
- _sources: new Set(['TestContract.sol', '/path/to/build-info/test.json'])
1691
- }
1692
-
1693
- context.contractRepository.addForTesting({
1694
- contractName: mockContract.contractName,
1695
- abi: mockContract.abi,
1696
- bytecode: mockContract.creationCode,
1697
- sourceName: mockContract.sourceName,
1698
- source: mockContract.source,
1699
- compiler: mockContract.compiler,
1700
- buildInfoId: mockContract.buildInfoId,
1701
- _path: '/test/path',
1702
- _hash: 'test-hash'
1703
- })
1704
-
1705
- const action: Action = {
1706
- type: 'verify-contract',
1707
- name: 'test-verify',
1708
- arguments: {
1709
- address: TEST_ADDRESSES.RECIPIENT_1,
1710
- contract: '{{Contract(TestContract)}}',
1711
- platform: 'mock-platform'
1712
- }
1713
- }
1714
-
1715
- // Should throw when ignoreVerifyErrors is disabled
1716
- await expect((engineWithoutIgnore as any).executePrimitive(action, context, new Map()))
1717
- .rejects.toThrow('Action "test-verify": No build-info file found in contract sources')
1718
- })
1719
-
1720
- it('should handle multiple platform verification failures with ignoreVerifyErrors', async () => {
1721
- const mockVerificationRegistry = new VerificationPlatformRegistry()
1722
-
1723
- // Create multiple failing platforms
1724
- const platforms = ['platform1', 'platform2', 'platform3']
1725
- platforms.forEach(name => {
1726
- const mockPlatform = {
1727
- name,
1728
- supportsNetwork: jest.fn().mockReturnValue(true),
1729
- isConfigured: jest.fn().mockReturnValue(true),
1730
- getConfigurationRequirements: jest.fn().mockReturnValue(''),
1731
- isContractAlreadyVerified: jest.fn().mockResolvedValue(false),
1732
- verifyContract: jest.fn().mockRejectedValue(new Error(`${name} verification failed`))
1733
- }
1734
- mockVerificationRegistry.register(mockPlatform)
1735
- })
1736
-
1737
- engine = new ExecutionEngine(templates, {
1738
- verificationRegistry: mockVerificationRegistry,
1739
- ignoreVerifyErrors: true
1740
- })
1741
-
1742
- const mockContract = {
1743
- sourceName: 'TestContract.sol',
1744
- contractName: 'TestContract',
1745
- compiler: { version: '0.8.0' },
1746
- buildInfoId: 'test-build-id',
1747
- source: 'contract TestContract { }',
1748
- creationCode: '0x608060405234801561000f575f5ffd5b50602a5f526020601ff3',
1749
- abi: [],
1750
- _sources: new Set(['TestContract.sol', '/path/to/build-info/test.json'])
1751
- }
1752
-
1753
- context.contractRepository.addForTesting({
1754
- contractName: mockContract.contractName,
1755
- abi: mockContract.abi,
1756
- bytecode: mockContract.creationCode,
1757
- sourceName: mockContract.sourceName,
1758
- source: mockContract.source,
1759
- compiler: mockContract.compiler,
1760
- buildInfoId: mockContract.buildInfoId,
1761
- _path: '/test/path',
1762
- _hash: 'test-hash'
1763
- })
1764
-
1765
- const action: Action = {
1766
- type: 'verify-contract',
1767
- name: 'test-verify',
1768
- arguments: {
1769
- address: TEST_ADDRESSES.RECIPIENT_1,
1770
- contract: '{{Contract(TestContract)}}',
1771
- platform: platforms
1772
- }
1773
- }
1774
-
1775
- // Should not throw even with multiple platform failures
1776
- await expect((engine as any).executePrimitive(action, context, new Map()))
1777
- .resolves.not.toThrow()
1778
-
1779
- // Should have collected warnings for all platforms
1780
- const warnings = engine.getVerificationWarnings()
1781
- expect(warnings).toHaveLength(3)
1782
-
1783
- platforms.forEach((platform, index) => {
1784
- expect(warnings[index]).toMatchObject({
1785
- actionName: 'test-verify',
1786
- platform,
1787
- error: 'Action "test-verify": No build-info file found in contract sources'
1788
- })
1789
- })
1790
- })
1791
-
1792
- it('should provide methods to get and clear verification warnings', () => {
1793
- // Initially empty
1794
- expect(engine.getVerificationWarnings()).toEqual([])
1795
-
1796
- // Manually add a warning (simulating what happens during verification)
1797
- ;(engine as any).verificationWarnings.push({
1798
- actionName: 'test',
1799
- address: '0x123',
1800
- contractName: 'Test',
1801
- platform: 'test-platform',
1802
- error: 'test error'
1803
- })
1804
-
1805
- // Should return the warning
1806
- const warnings = engine.getVerificationWarnings()
1807
- expect(warnings).toHaveLength(1)
1808
- expect(warnings[0]).toMatchObject({
1809
- actionName: 'test',
1810
- error: 'test error'
1811
- })
1812
-
1813
- // Clear warnings
1814
- engine.clearVerificationWarnings()
1815
- expect(engine.getVerificationWarnings()).toEqual([])
1816
- })
1817
-
1818
- it('should emit verification_skipped events when all platforms fail and ignoreVerifyErrors is enabled', async () => {
1819
- const mockVerificationRegistry = new VerificationPlatformRegistry()
1820
- const mockPlatform = {
1821
- name: 'mock-platform',
1822
- supportsNetwork: jest.fn().mockReturnValue(true),
1823
- isConfigured: jest.fn().mockReturnValue(true),
1824
- getConfigurationRequirements: jest.fn().mockReturnValue(''),
1825
- isContractAlreadyVerified: jest.fn().mockResolvedValue(false),
1826
- verifyContract: jest.fn().mockRejectedValue(new Error('Verification failed'))
1827
- }
1828
- mockVerificationRegistry.register(mockPlatform)
1829
- mockVerificationRegistry.getConfiguredPlatforms = jest.fn().mockReturnValue([mockPlatform])
1830
-
1831
- const mockEventEmitter = {
1832
- emitEvent: jest.fn()
1833
- }
1834
-
1835
- engine = new ExecutionEngine(templates, {
1836
- verificationRegistry: mockVerificationRegistry,
1837
- ignoreVerifyErrors: true,
1838
- eventEmitter: mockEventEmitter as any
1839
- })
1840
-
1841
- const mockContract = {
1842
- sourceName: 'TestContract.sol',
1843
- contractName: 'TestContract',
1844
- compiler: { version: '0.8.0' },
1845
- buildInfoId: 'test-build-id',
1846
- source: 'contract TestContract { }',
1847
- creationCode: '0x608060405234801561000f575f5ffd5b50602a5f526020601ff3',
1848
- abi: [],
1849
- _sources: new Set(['TestContract.sol', '/path/to/build-info/test.json'])
1850
- }
1851
-
1852
- context.contractRepository.addForTesting({
1853
- contractName: mockContract.contractName,
1854
- abi: mockContract.abi,
1855
- bytecode: mockContract.creationCode,
1856
- sourceName: mockContract.sourceName,
1857
- source: mockContract.source,
1858
- compiler: mockContract.compiler,
1859
- buildInfoId: mockContract.buildInfoId,
1860
- _path: '/test/path',
1861
- _hash: 'test-hash'
1862
- })
1863
-
1864
- const action: Action = {
1865
- type: 'verify-contract',
1866
- name: 'test-verify',
1867
- arguments: {
1868
- address: TEST_ADDRESSES.RECIPIENT_1,
1869
- contract: '{{Contract(TestContract)}}',
1870
- platform: 'all'
1871
- }
1872
- }
1873
-
1874
- await (engine as any).executePrimitive(action, context, new Map())
1875
-
1876
- // Should emit verification_skipped event
1877
- expect(mockEventEmitter.emitEvent).toHaveBeenCalledWith(
1878
- expect.objectContaining({
1879
- type: 'verification_skipped',
1880
- level: 'warn',
1881
- data: expect.objectContaining({
1882
- actionName: 'test-verify',
1883
- reason: expect.stringContaining('continuing due to --ignore-verify-errors')
1884
- })
1885
- })
1886
- )
1887
- })
1888
- })
1889
-
1890
- describe('skip_if behavior', () => {
1891
- it('should NOT post-check skip_if after job execution', async () => {
1892
- // This is the key semantic difference: skip_if is ONLY a pre-skip gate
1893
- // The pre-skip check for skip_if happens in the deployer, not in executeJob
1894
- // So executeJob runs normally without any post-check for skip_if
1895
- const jobWithSkipIf: Job = {
1896
- name: 'skip-if-job',
1897
- version: '1.0.0',
1898
- skip_if: [
1899
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }
1900
- ],
1901
- actions: [
1902
- {
1903
- name: 'generate-payload',
1904
- type: 'static',
1905
- arguments: { value: 'payload-data' }
1906
- }
1907
- ]
1908
- }
1909
-
1910
- // Set context so skip_if would evaluate to false if checked
1911
- context.setOutput('should_skip', 0)
1912
-
1913
- // This should succeed because skip_if is NOT post-checked in executeJob
1914
- // The job has no skip_condition, so no post-check happens
1915
- await expect(engine.executeJob(jobWithSkipIf, context)).resolves.not.toThrow()
1916
-
1917
- // Action should have been executed
1918
- expect(context.getOutput('generate-payload.value')).toBe('payload-data')
1919
- })
1920
-
1921
- it('should post-check skip_condition but NOT skip_if', async () => {
1922
- // Job with skip_condition - should post-check and fail
1923
- const jobWithSkipCondition: Job = {
1924
- name: 'skip-condition-job',
1925
- version: '1.0.0',
1926
- skip_condition: [
1927
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{converged}}', 1] } }
1928
- ],
1929
- actions: [
1930
- {
1931
- name: 'do-work',
1932
- type: 'static',
1933
- arguments: { value: 'work-done' }
1934
- }
1935
- ]
1936
- }
1937
-
1938
- // Pre-execution: skip_condition is false (0) -> job runs
1939
- context.setOutput('converged', 0)
1940
-
1941
- // Post-execution: skip_condition is STILL false -> should FAIL
1942
- // The post-check re-evaluates skip_condition and throws if not true
1943
- await expect(engine.executeJob(jobWithSkipCondition, context)).rejects.toThrow('failed post-execution check')
1944
- })
1945
-
1946
- it('should not post-check skip_if (only skip_condition is post-checked)', async () => {
1947
- // Job with only skip_if - no post-check should happen
1948
- const jobWithOnlySkipIf: Job = {
1949
- name: 'skip-if-only-job',
1950
- version: '1.0.0',
1951
- skip_if: [
1952
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }
1953
- ],
1954
- actions: [
1955
- {
1956
- name: 'generate-artifact',
1957
- type: 'static',
1958
- arguments: { value: 'artifact-data' }
1959
- }
1960
- ]
1961
- }
1962
-
1963
- // Set context so skip_if would be true if checked post-execution
1964
- context.setOutput('should_skip', 1)
1965
-
1966
- // But since skip_if is NOT post-checked (only skip_condition is),
1967
- // and the job has no skip_condition, no post-check happens
1968
- await expect(engine.executeJob(jobWithOnlySkipIf, context)).resolves.not.toThrow()
1969
-
1970
- // Action should have been executed
1971
- expect(context.getOutput('generate-artifact.value')).toBe('artifact-data')
1972
- })
1973
-
1974
- it('should post-check skip_condition when both skip_if and skip_condition are present', async () => {
1975
- // Job with BOTH skip_if and skip_condition
1976
- // skip_if is only pre-checked (in deployer)
1977
- // skip_condition is both pre-checked and post-checked
1978
- const jobWithBoth: Job = {
1979
- name: 'both-conditions-job',
1980
- version: '1.0.0',
1981
- skip_condition: [
1982
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{converged}}', 1] } }
1983
- ],
1984
- skip_if: [
1985
- { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }
1986
- ],
1987
- actions: [
1988
- {
1989
- name: 'do-work',
1990
- type: 'static',
1991
- arguments: { value: 'work-done' }
1992
- }
1993
- ]
1994
- }
1995
-
1996
- // Pre-execution: both conditions are false -> job runs
1997
- context.setOutput('should_skip', 0)
1998
- context.setOutput('converged', 0)
1999
-
2000
- // Post-execution: skip_condition is STILL false -> should FAIL
2001
- // skip_if is NOT post-checked
2002
- await expect(engine.executeJob(jobWithBoth, context)).rejects.toThrow('failed post-execution check')
2003
- })
2004
- })
2005
- })