@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.
- package/README.md +27 -0
- package/dist/lib/__tests__/network-loader.spec.js.map +1 -1
- package/dist/lib/core/__tests__/resolver.spec.js +22 -0
- package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
- package/dist/lib/core/__tests__/sign-actions.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/sign-actions.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/sign-actions.spec.js +128 -0
- package/dist/lib/core/__tests__/sign-actions.spec.js.map +1 -0
- package/dist/lib/core/__tests__/signer.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/signer.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/signer.spec.js +40 -0
- package/dist/lib/core/__tests__/signer.spec.js.map +1 -0
- package/dist/lib/core/context.d.ts +3 -2
- package/dist/lib/core/context.d.ts.map +1 -1
- package/dist/lib/core/context.js +3 -2
- package/dist/lib/core/context.js.map +1 -1
- package/dist/lib/core/engine.d.ts +4 -0
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +173 -0
- package/dist/lib/core/engine.js.map +1 -1
- package/dist/lib/core/signer.d.ts +7 -0
- package/dist/lib/core/signer.d.ts.map +1 -0
- package/dist/lib/core/signer.js +60 -0
- package/dist/lib/core/signer.js.map +1 -0
- package/dist/lib/parsers/__tests__/source.spec.js +37 -0
- package/dist/lib/parsers/__tests__/source.spec.js.map +1 -1
- package/dist/lib/parsers/source.js +1 -1
- package/dist/lib/parsers/source.js.map +1 -1
- package/dist/lib/provenance.js +51 -2
- package/dist/lib/provenance.js.map +1 -1
- package/dist/lib/types/actions.d.ts +26 -2
- package/dist/lib/types/actions.d.ts.map +1 -1
- package/dist/lib/types/actions.js +3 -0
- package/dist/lib/types/actions.js.map +1 -1
- package/dist/lib/types/source.d.ts +2 -0
- package/dist/lib/types/source.d.ts.map +1 -1
- package/package.json +4 -1
- package/.eslintrc.json +0 -29
- package/.github/workflows/ci.yml +0 -181
- package/CONCEPT.md +0 -24
- package/contracts/checked-call.huff +0 -65
- package/eslint.config.js +0 -48
- package/examples/jobs/guards-v1.yaml +0 -17
- package/examples/jobs/sequence-seq-0001-patch.yaml +0 -59
- package/examples/jobs/sequence-v1.yaml +0 -59
- package/examples/templates/sequence-factory-v1.yaml +0 -56
- package/jest.config.js +0 -25
- package/src/cli.ts +0 -18
- package/src/commands/common.ts +0 -61
- package/src/commands/dry.ts +0 -209
- package/src/commands/etherscan.ts +0 -360
- package/src/commands/index.ts +0 -6
- package/src/commands/list.ts +0 -262
- package/src/commands/provenance.ts +0 -120
- package/src/commands/run.ts +0 -146
- package/src/commands/utils.ts +0 -215
- package/src/index.ts +0 -67
- package/src/lib/__tests__/deployer-events.spec.ts +0 -338
- package/src/lib/__tests__/deployer.spec.ts +0 -2269
- package/src/lib/__tests__/network-loader.spec.ts +0 -150
- package/src/lib/__tests__/network-selection.spec.ts +0 -41
- package/src/lib/__tests__/network-utils.spec.ts +0 -230
- package/src/lib/__tests__/provenance.spec.ts +0 -208
- package/src/lib/artifacts/__tests__/fixtures/contract1.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/contract2.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/duplicate-name.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/nested/nested-contract.json +0 -18
- package/src/lib/artifacts/__tests__/fixtures/not-an-artifact.json +0 -8
- package/src/lib/artifacts/__tests__/fixtures/readme.txt +0 -2
- package/src/lib/contracts/__tests__/repository.spec.ts +0 -612
- package/src/lib/contracts/repository.ts +0 -411
- package/src/lib/core/__tests__/assert-action.spec.ts +0 -474
- package/src/lib/core/__tests__/context.spec.ts +0 -37
- package/src/lib/core/__tests__/engine.spec.ts +0 -2005
- package/src/lib/core/__tests__/graph.spec.ts +0 -125
- package/src/lib/core/__tests__/json-integration.spec.ts +0 -425
- package/src/lib/core/__tests__/loader.spec.ts +0 -367
- package/src/lib/core/__tests__/multi-platform-verification.spec.ts +0 -406
- package/src/lib/core/__tests__/resolver.spec.ts +0 -2496
- package/src/lib/core/__tests__/static-action.spec.ts +0 -172
- package/src/lib/core/context.ts +0 -127
- package/src/lib/core/engine.ts +0 -1834
- package/src/lib/core/graph.ts +0 -252
- package/src/lib/core/loader.ts +0 -253
- package/src/lib/core/resolver.ts +0 -873
- package/src/lib/deployer.ts +0 -1005
- package/src/lib/events/__tests__/event-system.spec.ts +0 -392
- package/src/lib/events/cli-adapter.ts +0 -369
- package/src/lib/events/emitter.ts +0 -62
- package/src/lib/events/index.ts +0 -3
- package/src/lib/events/types.ts +0 -520
- package/src/lib/index.ts +0 -17
- package/src/lib/network-loader.ts +0 -90
- package/src/lib/network-selection.ts +0 -73
- package/src/lib/network-utils.ts +0 -64
- package/src/lib/parsers/__tests__/buildinfo.spec.ts +0 -122
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-bytecode-buildinfo.json +0 -62
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-json.txt +0 -2
- package/src/lib/parsers/__tests__/fixtures/buildinfo/multi-contract-buildinfo.json +0 -89
- package/src/lib/parsers/__tests__/fixtures/buildinfo/no-contracts-buildinfo.json +0 -17
- package/src/lib/parsers/__tests__/fixtures/buildinfo/simple-buildinfo.json +0 -63
- package/src/lib/parsers/__tests__/fixtures/buildinfo/wrong-format.json +0 -4
- package/src/lib/parsers/__tests__/job.spec.ts +0 -439
- package/src/lib/parsers/__tests__/source.spec.ts +0 -134
- package/src/lib/parsers/__tests__/template.spec.ts +0 -111
- package/src/lib/parsers/artifact/__tests__/artifact.spec.ts +0 -117
- package/src/lib/parsers/artifact/__tests__/fixtures/empty-bytecode.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/hardhat-artifact.json +0 -67
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-bytecode.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-json.txt +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/minimal-artifact.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-abi.json +0 -4
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-bytecode.json +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-contract-name.json +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/simple-artifact.json +0 -40
- package/src/lib/parsers/artifact/__tests__/fixtures/wrong-types.json +0 -7
- package/src/lib/parsers/artifact/foundry-1.2.ts +0 -72
- package/src/lib/parsers/artifact/index.ts +0 -27
- package/src/lib/parsers/artifact/types.ts +0 -9
- package/src/lib/parsers/buildinfo.ts +0 -127
- package/src/lib/parsers/constants.ts +0 -56
- package/src/lib/parsers/index.ts +0 -6
- package/src/lib/parsers/job.ts +0 -160
- package/src/lib/parsers/source.ts +0 -129
- package/src/lib/parsers/template.ts +0 -135
- package/src/lib/provenance.ts +0 -785
- package/src/lib/std/templates/arachnid-deterministic-deployment-proxy.yaml +0 -68
- package/src/lib/std/templates/assured-deployment.yaml +0 -46
- package/src/lib/std/templates/era-evm-predeploy.yaml +0 -35
- package/src/lib/std/templates/erc-2470.yaml +0 -70
- package/src/lib/std/templates/min-balance.yaml +0 -35
- package/src/lib/std/templates/nano-universal-deployer.yaml +0 -61
- package/src/lib/std/templates/raw-erc-2470.yaml +0 -62
- package/src/lib/std/templates/raw-nano-universal-deployer.yaml +0 -54
- package/src/lib/std/templates/raw-sequence-universal-deployer-2.yaml +0 -52
- package/src/lib/std/templates/sequence-universal-deployer-2.yaml +0 -61
- package/src/lib/types/__tests__/json-request-action.spec.ts +0 -243
- package/src/lib/types/__tests__/read-json-value.spec.ts +0 -278
- package/src/lib/types/__tests__/resolve-json-value.spec.ts +0 -769
- package/src/lib/types/actions.ts +0 -148
- package/src/lib/types/artifacts.ts +0 -21
- package/src/lib/types/buildinfo.ts +0 -116
- package/src/lib/types/conditions.ts +0 -50
- package/src/lib/types/contracts.ts +0 -26
- package/src/lib/types/definitions.ts +0 -77
- package/src/lib/types/index.ts +0 -9
- package/src/lib/types/network.ts +0 -33
- package/src/lib/types/project.ts +0 -9
- package/src/lib/types/source.ts +0 -26
- package/src/lib/types/task.ts +0 -9
- package/src/lib/types/values.ts +0 -221
- package/src/lib/utils/assertion.ts +0 -24
- package/src/lib/utils/validation.ts +0 -116
- package/src/lib/validation/contract-references.ts +0 -210
- package/src/lib/validation/index.ts +0 -1
- package/src/lib/verification/__tests__/etherscan.spec.ts +0 -710
- package/src/lib/verification/__tests__/sourcify.spec.ts +0 -288
- package/src/lib/verification/etherscan.ts +0 -547
- package/src/lib/verification/sourcify.ts +0 -248
- package/test_validation/artifacts/TestContract.json +0 -9
- package/test_validation/jobs/test-missing.yaml +0 -16
- package/test_validation/networks.yaml +0 -3
- 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
|
-
})
|