@0xsequence/catapult 1.2.1 ā 1.3.2
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 +42 -14
- package/dist/commands/dry.d.ts.map +1 -1
- package/dist/commands/dry.js +3 -2
- package/dist/commands/dry.js.map +1 -1
- package/dist/commands/etherscan.d.ts.map +1 -1
- package/dist/commands/etherscan.js +24 -36
- package/dist/commands/etherscan.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +4 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/utils.d.ts.map +1 -1
- package/dist/commands/utils.js +191 -0
- package/dist/commands/utils.js.map +1 -1
- package/dist/lib/__tests__/deployer.spec.js +1 -1
- package/dist/lib/__tests__/deployer.spec.js.map +1 -1
- package/dist/lib/__tests__/network-loader.spec.js +3 -2
- package/dist/lib/__tests__/network-loader.spec.js.map +1 -1
- package/dist/lib/__tests__/network-selection.spec.d.ts +2 -0
- package/dist/lib/__tests__/network-selection.spec.d.ts.map +1 -0
- package/dist/lib/__tests__/network-selection.spec.js +34 -0
- package/dist/lib/__tests__/network-selection.spec.js.map +1 -0
- package/dist/lib/core/__tests__/engine.spec.js +26 -2
- package/dist/lib/core/__tests__/engine.spec.js.map +1 -1
- package/dist/lib/core/__tests__/json-integration.spec.js +1 -1
- package/dist/lib/core/__tests__/json-integration.spec.js.map +1 -1
- package/dist/lib/core/__tests__/multi-platform-verification.spec.js +1 -1
- package/dist/lib/core/__tests__/multi-platform-verification.spec.js.map +1 -1
- package/dist/lib/core/__tests__/static-action.spec.js +1 -1
- package/dist/lib/core/__tests__/static-action.spec.js.map +1 -1
- package/dist/lib/core/engine.d.ts +9 -1
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +17 -4
- package/dist/lib/core/engine.js.map +1 -1
- package/dist/lib/deployer.d.ts.map +1 -1
- package/dist/lib/deployer.js +56 -2
- package/dist/lib/deployer.js.map +1 -1
- package/dist/lib/events/cli-adapter.d.ts.map +1 -1
- package/dist/lib/events/cli-adapter.js +16 -3
- package/dist/lib/events/cli-adapter.js.map +1 -1
- package/dist/lib/events/types.d.ts +18 -1
- package/dist/lib/events/types.d.ts.map +1 -1
- package/dist/lib/network-loader.d.ts.map +1 -1
- package/dist/lib/network-loader.js +1 -1
- package/dist/lib/network-loader.js.map +1 -1
- package/dist/lib/network-selection.d.ts +4 -0
- package/dist/lib/network-selection.d.ts.map +1 -0
- package/dist/lib/network-selection.js +49 -0
- package/dist/lib/network-selection.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/dry.ts +4 -3
- package/src/commands/etherscan.ts +42 -42
- package/src/commands/run.ts +5 -3
- package/src/commands/utils.ts +163 -0
- package/src/lib/__tests__/deployer.spec.ts +1 -3
- package/src/lib/__tests__/network-loader.spec.ts +3 -2
- package/src/lib/__tests__/network-selection.spec.ts +41 -0
- package/src/lib/core/__tests__/engine.spec.ts +33 -2
- package/src/lib/core/__tests__/json-integration.spec.ts +1 -1
- package/src/lib/core/__tests__/multi-platform-verification.spec.ts +1 -1
- package/src/lib/core/__tests__/static-action.spec.ts +1 -1
- package/src/lib/core/engine.ts +26 -4
- package/src/lib/deployer.ts +68 -3
- package/src/lib/events/cli-adapter.ts +21 -5
- package/src/lib/events/types.ts +23 -0
- package/src/lib/network-loader.ts +2 -1
- package/src/lib/network-selection.ts +73 -0
- package/CONCEPT.md +0 -24
package/src/commands/utils.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
+
import * as fs from 'fs'
|
|
4
|
+
import * as path from 'path'
|
|
3
5
|
import { projectOption, verbosityOption } from './common'
|
|
4
6
|
import { loadNetworks } from '../lib/network-loader'
|
|
5
7
|
import { setVerbosity } from '../index'
|
|
@@ -48,5 +50,166 @@ export function makeUtilsCommand(): Command {
|
|
|
48
50
|
|
|
49
51
|
utils.addCommand(chainIdToName)
|
|
50
52
|
|
|
53
|
+
// utils gen-table <output-dir>
|
|
54
|
+
const genTable = new Command('gen-table')
|
|
55
|
+
.description('Generate a consolidated addresses table from an output directory')
|
|
56
|
+
.argument('<output-dir>', 'Directory containing job output JSON files (searches recursively)')
|
|
57
|
+
.option('--name', 'Include Name column', true)
|
|
58
|
+
.option('--key', 'Include Key column', false)
|
|
59
|
+
.option('--file', 'Include File column', false)
|
|
60
|
+
.option('--chain-ids, --chainIds', 'Include ChainIds column', false)
|
|
61
|
+
.option('--job', 'Include Job column', true)
|
|
62
|
+
.option('--address', 'Include Address column', true)
|
|
63
|
+
.option('--format <format>', "Output format: 'markdown' or 'ascii' (default)", 'ascii')
|
|
64
|
+
.action(async (outputDir: string, options: { name?: boolean; key?: boolean; file?: boolean; chainIds?: boolean; job?: boolean; address?: boolean; format?: string }) => {
|
|
65
|
+
try {
|
|
66
|
+
const absoluteDir = path.resolve(outputDir)
|
|
67
|
+
if (!fs.existsSync(absoluteDir) || !fs.statSync(absoluteDir).isDirectory()) {
|
|
68
|
+
console.error(chalk.red(`Output directory not found or not a directory: ${absoluteDir}`))
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const jsonFiles: string[] = []
|
|
73
|
+
const walk = (dir: string) => {
|
|
74
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
75
|
+
const full = path.join(dir, entry)
|
|
76
|
+
const stat = fs.statSync(full)
|
|
77
|
+
if (stat.isDirectory()) walk(full)
|
|
78
|
+
else if (stat.isFile() && entry.toLowerCase().endsWith('.json')) jsonFiles.push(full)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
walk(absoluteDir)
|
|
82
|
+
|
|
83
|
+
type Row = { job: string; chainIds: string; name: string; address: string; key: string; file: string }
|
|
84
|
+
const rows: Row[] = []
|
|
85
|
+
const addressRegex = /^0x[a-fA-F0-9]{40}$/
|
|
86
|
+
|
|
87
|
+
const toTitleCase = (slug: string): string => slug.split(/[-_\s]+/).filter(Boolean).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('')
|
|
88
|
+
const extractVersionSuffix = (jobName: string): string => {
|
|
89
|
+
const m = jobName.match(/[-_]?v(\d+)/i)
|
|
90
|
+
return m ? `V${m[1]}` : ''
|
|
91
|
+
}
|
|
92
|
+
const deriveName = (jobName: string, key: string): string => {
|
|
93
|
+
const version = extractVersionSuffix(jobName)
|
|
94
|
+
const baseJob = jobName.replace(/[-_]?v\d+$/i, '')
|
|
95
|
+
const keyCore = key.replace(/\.address$/i, '')
|
|
96
|
+
// Prefer descriptive key name; if too generic like 'factory', prefix with job base
|
|
97
|
+
const isGeneric = /^(factory|address)$/i.test(keyCore)
|
|
98
|
+
const nameCore = isGeneric ? `${toTitleCase(baseJob)} ${toTitleCase(keyCore)}` : toTitleCase(keyCore)
|
|
99
|
+
return `${nameCore.replace(/\s+/g, '')}${version}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const file of jsonFiles) {
|
|
103
|
+
try {
|
|
104
|
+
const raw = fs.readFileSync(file, 'utf8')
|
|
105
|
+
const data = JSON.parse(raw)
|
|
106
|
+
if (!data || typeof data !== 'object' || !Array.isArray(data.networks)) continue
|
|
107
|
+
const jobName: string = data.jobName ?? path.basename(file, '.json')
|
|
108
|
+
for (const net of data.networks) {
|
|
109
|
+
if (!net || typeof net !== 'object') continue
|
|
110
|
+
const outputs = net.outputs as Record<string, unknown> | undefined
|
|
111
|
+
if (!outputs) continue
|
|
112
|
+
const chainIds: string[] = Array.isArray(net.chainIds) ? net.chainIds : []
|
|
113
|
+
for (const [key, value] of Object.entries(outputs)) {
|
|
114
|
+
let address: string | undefined
|
|
115
|
+
if (typeof value === 'string' && addressRegex.test(value)) {
|
|
116
|
+
address = value
|
|
117
|
+
} else if (value && typeof value === 'object' && 'address' in value && typeof value.address === 'string' && addressRegex.test(value.address)) {
|
|
118
|
+
address = value.address
|
|
119
|
+
}
|
|
120
|
+
if (!address) continue
|
|
121
|
+
rows.push({
|
|
122
|
+
job: jobName,
|
|
123
|
+
chainIds: chainIds.join(','),
|
|
124
|
+
name: deriveName(jobName, key),
|
|
125
|
+
address,
|
|
126
|
+
key,
|
|
127
|
+
file
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// skip invalid JSON
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
rows.sort((a, b) => a.job.localeCompare(b.job) || a.name.localeCompare(b.name))
|
|
137
|
+
|
|
138
|
+
if (rows.length === 0) {
|
|
139
|
+
console.log(chalk.yellow('No address entries found.'))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Determine which columns to show
|
|
144
|
+
const showJob = !!options.job
|
|
145
|
+
const showAddress = !!options.address
|
|
146
|
+
const showName = !!options.name
|
|
147
|
+
const showKey = !!options.key
|
|
148
|
+
const showChainIds = !!options.chainIds
|
|
149
|
+
const showFile = !!options.file
|
|
150
|
+
|
|
151
|
+
const selectedHeaders: (keyof Row)[] = []
|
|
152
|
+
if (showJob) selectedHeaders.push('job')
|
|
153
|
+
if (showChainIds) selectedHeaders.push('chainIds')
|
|
154
|
+
if (showName) selectedHeaders.push('name')
|
|
155
|
+
if (showAddress) selectedHeaders.push('address')
|
|
156
|
+
if (showKey) selectedHeaders.push('key')
|
|
157
|
+
if (showFile) selectedHeaders.push('file')
|
|
158
|
+
|
|
159
|
+
// Titles for columns
|
|
160
|
+
const titles: Record<keyof Row, string> = {
|
|
161
|
+
job: 'Job',
|
|
162
|
+
chainIds: 'ChainIds',
|
|
163
|
+
name: 'Name',
|
|
164
|
+
address: 'Address',
|
|
165
|
+
key: 'Key',
|
|
166
|
+
file: 'File'
|
|
167
|
+
}
|
|
168
|
+
const format = String(options.format || 'markdown').toLowerCase()
|
|
169
|
+
if (format !== 'markdown' && format !== 'ascii') {
|
|
170
|
+
console.error(chalk.red("Invalid format. Use 'markdown' or 'ascii'."))
|
|
171
|
+
process.exit(1)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (format === 'markdown') {
|
|
175
|
+
const header = '| ' + selectedHeaders.map(h => titles[h]).join(' | ') + ' |'
|
|
176
|
+
const sepMd = '| ' + selectedHeaders.map(h => '-'.repeat(Math.max(3, String(titles[h]).length))).join(' | ') + ' |'
|
|
177
|
+
console.log(header)
|
|
178
|
+
console.log(sepMd)
|
|
179
|
+
for (const r of rows) {
|
|
180
|
+
console.log('| ' + selectedHeaders.map(h => String(r[h])).join(' | ') + ' |')
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// ascii rendering with box-drawing characters
|
|
184
|
+
const widths: Record<string, number> = {}
|
|
185
|
+
for (const h of selectedHeaders) {
|
|
186
|
+
widths[h] = Math.max(titles[h].length, ...rows.map(r => String(r[h]).length))
|
|
187
|
+
}
|
|
188
|
+
const makeSep = (left: string, mid: string, right: string, fill: string) => {
|
|
189
|
+
return left + selectedHeaders.map(h => fill.repeat(widths[h] + 2)).join(mid) + right
|
|
190
|
+
}
|
|
191
|
+
const pad = (s: string, w: number) => s + ' '.repeat(Math.max(0, w - s.length))
|
|
192
|
+
|
|
193
|
+
const top = makeSep('ā', 'ā¬', 'ā', 'ā')
|
|
194
|
+
const sep = makeSep('ā', 'ā¼', 'ā¤', 'ā')
|
|
195
|
+
const bot = makeSep('ā', 'ā“', 'ā', 'ā')
|
|
196
|
+
const headerLine = 'ā' + selectedHeaders.map(h => ' ' + pad(titles[h], widths[h]) + ' ').join('ā') + 'ā'
|
|
197
|
+
const lines = rows.map(r => 'ā' + selectedHeaders.map(h => ' ' + pad(String(r[h]), widths[h]) + ' ').join('ā') + 'ā')
|
|
198
|
+
|
|
199
|
+
console.log(top)
|
|
200
|
+
console.log(headerLine)
|
|
201
|
+
console.log(sep)
|
|
202
|
+
for (const line of lines) console.log(line)
|
|
203
|
+
console.log(bot)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(chalk.red('Error generating table:'), error instanceof Error ? error.message : String(error))
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
utils.addCommand(genTable)
|
|
213
|
+
|
|
51
214
|
return utils
|
|
52
215
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import * as fs from 'fs/promises'
|
|
2
|
-
import * as path from 'path'
|
|
3
|
-
import chalk from 'chalk'
|
|
4
2
|
import { Deployer, DeployerOptions } from '../deployer'
|
|
5
3
|
import { ProjectLoader } from '../core/loader'
|
|
6
4
|
import { DependencyGraph } from '../core/graph'
|
|
@@ -174,7 +172,7 @@ describe('Deployer', () => {
|
|
|
174
172
|
expect(mockLoader.load).toHaveBeenCalledTimes(1)
|
|
175
173
|
expect(MockDependencyGraph).toHaveBeenCalledWith(mockLoader.jobs, mockLoader.templates)
|
|
176
174
|
expect(mockGraph.getExecutionOrder).toHaveBeenCalledTimes(1)
|
|
177
|
-
expect(MockExecutionEngine).toHaveBeenCalledWith(mockLoader.templates, expect.any(Object)
|
|
175
|
+
expect(MockExecutionEngine).toHaveBeenCalledWith(mockLoader.templates, expect.any(Object))
|
|
178
176
|
expect(mockEngine.executeJob).toHaveBeenCalledTimes(5) // job1&job2 on 2 networks + job3 on 1 network
|
|
179
177
|
expect(MockExecutionContext).toHaveBeenCalledTimes(5)
|
|
180
178
|
expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output', { recursive: true })
|
|
@@ -84,7 +84,7 @@ describe('network-loader rpcUrl token replacement', () => {
|
|
|
84
84
|
expect(networks[0].rpcUrl).toBe('https://node.example.com/XYZ')
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
-
test('
|
|
87
|
+
test('defaults to empty string when RPC token has no matching env var', async () => {
|
|
88
88
|
const projectRoot = path.join(tmpDir, 'case5')
|
|
89
89
|
delete process.env.RPC_MISSING
|
|
90
90
|
const yaml = `
|
|
@@ -94,6 +94,7 @@ describe('network-loader rpcUrl token replacement', () => {
|
|
|
94
94
|
`
|
|
95
95
|
await writeNetworksYaml(projectRoot, yaml)
|
|
96
96
|
|
|
97
|
-
await
|
|
97
|
+
const networks = await loadNetworks(projectRoot)
|
|
98
|
+
expect(networks[0].rpcUrl).toBe('https://node.example.com/')
|
|
98
99
|
})
|
|
99
100
|
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveSelectedChainIds, resolveSingleChainId } from '../../lib/network-selection'
|
|
2
|
+
import { Network } from '../../lib/types'
|
|
3
|
+
|
|
4
|
+
describe('network-selection', () => {
|
|
5
|
+
const networks: Network[] = [
|
|
6
|
+
{ name: 'Mainnet', chainId: 1, rpcUrl: 'https://mainnet' },
|
|
7
|
+
{ name: 'Arbitrum One', chainId: 42161, rpcUrl: 'https://arb' },
|
|
8
|
+
{ name: 'Polygon', chainId: 137, rpcUrl: 'https://polygon' },
|
|
9
|
+
{ name: 'Mainnet', chainId: 10_000, rpcUrl: 'https://fork' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
it('returns undefined for empty/undefined input', () => {
|
|
13
|
+
expect(resolveSelectedChainIds(undefined, networks)).toBeUndefined()
|
|
14
|
+
expect(resolveSelectedChainIds('', networks)).toBeUndefined()
|
|
15
|
+
expect(resolveSelectedChainIds(' ', networks)).toBeUndefined()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('parses numeric IDs and removes duplicates', () => {
|
|
19
|
+
expect(resolveSelectedChainIds('1,1,42161', networks)).toEqual([1, 42161])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('matches names case-insensitively and includes all with same name', () => {
|
|
23
|
+
expect(resolveSelectedChainIds('mainnet', networks)).toEqual([1, 10000])
|
|
24
|
+
expect(resolveSelectedChainIds('MAINNET,polygon', networks)).toEqual([1, 10000, 137])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('preserves token order and then network order for name matches', () => {
|
|
28
|
+
expect(resolveSelectedChainIds('polygon,mainnet', networks)).toEqual([137, 1, 10000])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('throws on unknown name', () => {
|
|
32
|
+
expect(() => resolveSelectedChainIds('unknown', networks)).toThrow(/Unknown network selector/)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('resolveSingleChainId returns first match for name and first token for multi', () => {
|
|
36
|
+
expect(resolveSingleChainId('mainnet', networks)).toBe(1)
|
|
37
|
+
expect(resolveSingleChainId('137,1', networks)).toBe(137)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
|
|
@@ -26,7 +26,9 @@ const TEST_BYTECODES = {
|
|
|
26
26
|
// Mini contract with multiplication function
|
|
27
27
|
MINI_CONTRACT: '0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80636df5b97a14610038578063f8a8fd6d14610068575b5f5ffd5b610052600480360381019061004d91906100da565b610086565b60405161005f9190610127565b60405180910390f35b61007061009b565b60405161007d9190610127565b60405180910390f35b5f8183610093919061016d565b905092915050565b5f602a905090565b5f5ffd5b5f819050919050565b6100b9816100a7565b81146100c3575f5ffd5b50565b5f813590506100d4816100b0565b92915050565b5f5f604083850312156100f0576100ef6100a3565b5b5f6100fd858286016100c6565b925050602061010e858286016100c6565b9150509250929050565b610121816100a7565b82525050565b5f60208201905061013a5f830184610118565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610177826100a7565b9150610182836100a7565b9250828202610190816100a7565b915082820484148315176101a7576101a6610140565b5b509291505056fea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033',
|
|
28
28
|
// Minimal deployment bytecode
|
|
29
|
-
MINIMAL_DEPLOY: '0x608060405234801561000f575f5ffd5b50603e80601c5f395ff3fe60806040525f80fdfea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033'
|
|
29
|
+
MINIMAL_DEPLOY: '0x608060405234801561000f575f5ffd5b50603e80601c5f395ff3fe60806040525f80fdfea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033',
|
|
30
|
+
// Broken bytecode
|
|
31
|
+
BROKEN_BYTECODE: '0xff',
|
|
30
32
|
} as const
|
|
31
33
|
|
|
32
34
|
const TEST_VALUES = {
|
|
@@ -68,7 +70,7 @@ describe('ExecutionEngine', () => {
|
|
|
68
70
|
|
|
69
71
|
// Create empty verification registry for tests
|
|
70
72
|
const verificationRegistry = new VerificationPlatformRegistry()
|
|
71
|
-
engine = new ExecutionEngine(templates,
|
|
73
|
+
engine = new ExecutionEngine(templates, { verificationRegistry })
|
|
72
74
|
})
|
|
73
75
|
|
|
74
76
|
afterEach(async () => {
|
|
@@ -1480,6 +1482,35 @@ describe('ExecutionEngine', () => {
|
|
|
1480
1482
|
expect(context.getOutput('nick-test.success')).toBe(true)
|
|
1481
1483
|
})
|
|
1482
1484
|
|
|
1485
|
+
it('should prevent failing Nick\'s method from being tested twice', async () => {
|
|
1486
|
+
const action: Action = {
|
|
1487
|
+
type: 'test-nicks-method',
|
|
1488
|
+
name: 'nick-test-fail-twice',
|
|
1489
|
+
arguments: {
|
|
1490
|
+
bytecode: TEST_BYTECODES.BROKEN_BYTECODE, // Will break
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
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"`))
|
|
1495
|
+
await expect((engine as any).executeAction(action, context, new Map())).rejects.toThrow(new Error('Nick\'s method test already performed this run'))
|
|
1496
|
+
})
|
|
1497
|
+
|
|
1498
|
+
it('should prevent passing Nick\'s method from being tested twice', async () => {
|
|
1499
|
+
const action: Action = {
|
|
1500
|
+
type: 'test-nicks-method',
|
|
1501
|
+
name: 'nick-test-pass-twice',
|
|
1502
|
+
arguments: {
|
|
1503
|
+
bytecode: TEST_BYTECODES.SIMPLE_RETURN_42, // Will pass
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
await (engine as any).executeAction(action, context, new Map())
|
|
1508
|
+
await (engine as any).executeAction(action, context, new Map())
|
|
1509
|
+
|
|
1510
|
+
// Should have success output
|
|
1511
|
+
expect(context.getOutput('nick-test-pass-twice.success')).toBe(true)
|
|
1512
|
+
})
|
|
1513
|
+
|
|
1483
1514
|
it('should handle missing optional parameters', async () => {
|
|
1484
1515
|
const action: Action = {
|
|
1485
1516
|
type: 'test-nicks-method',
|
|
@@ -31,7 +31,7 @@ describe('JSON Integration Tests', () => {
|
|
|
31
31
|
|
|
32
32
|
templates = new Map()
|
|
33
33
|
const verificationRegistry = new VerificationPlatformRegistry()
|
|
34
|
-
engine = new ExecutionEngine(templates,
|
|
34
|
+
engine = new ExecutionEngine(templates, { verificationRegistry })
|
|
35
35
|
resolver = new ValueResolver()
|
|
36
36
|
})
|
|
37
37
|
|
|
@@ -68,7 +68,7 @@ describe('Multi-Platform Verification Integration', () => {
|
|
|
68
68
|
verificationRegistry.register(mockSourcifyPlatform)
|
|
69
69
|
|
|
70
70
|
templates = new Map()
|
|
71
|
-
engine = new ExecutionEngine(templates,
|
|
71
|
+
engine = new ExecutionEngine(templates, { verificationRegistry })
|
|
72
72
|
|
|
73
73
|
// Set up mock contract and build info
|
|
74
74
|
const mockContract = {
|
|
@@ -27,7 +27,7 @@ describe('Static Action', () => {
|
|
|
27
27
|
|
|
28
28
|
templates = new Map()
|
|
29
29
|
const verificationRegistry = new VerificationPlatformRegistry()
|
|
30
|
-
engine = new ExecutionEngine(templates,
|
|
30
|
+
engine = new ExecutionEngine(templates, { verificationRegistry })
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
describe('static primitive action', () => {
|
package/src/lib/core/engine.ts
CHANGED
|
@@ -8,6 +8,13 @@ import { createDefaultVerificationRegistry, VerificationPlatformRegistry } from
|
|
|
8
8
|
import { BuildInfo } from '../types/buildinfo'
|
|
9
9
|
import { ethers } from 'ethers'
|
|
10
10
|
|
|
11
|
+
export type EngineOptions = {
|
|
12
|
+
eventEmitter?: DeploymentEventEmitter
|
|
13
|
+
verificationRegistry?: VerificationPlatformRegistry
|
|
14
|
+
noPostCheckConditions?: boolean
|
|
15
|
+
allowMultipleNicksMethodTests?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* The ExecutionEngine is the core component that runs jobs and their actions.
|
|
13
20
|
* It interprets the declarative YAML files, resolves values, interacts with the
|
|
@@ -19,13 +26,16 @@ export class ExecutionEngine {
|
|
|
19
26
|
private readonly events: DeploymentEventEmitter
|
|
20
27
|
private readonly verificationRegistry: VerificationPlatformRegistry
|
|
21
28
|
private readonly noPostCheckConditions: boolean
|
|
29
|
+
private readonly allowMultipleNicksMethodTests: boolean
|
|
30
|
+
private nicksMethodTested: boolean = false
|
|
22
31
|
|
|
23
|
-
constructor(templates: Map<string, Template>,
|
|
32
|
+
constructor(templates: Map<string, Template>, options?: EngineOptions) {
|
|
24
33
|
this.resolver = new ValueResolver()
|
|
25
34
|
this.templates = templates
|
|
26
|
-
this.events = eventEmitter || deploymentEvents
|
|
27
|
-
this.verificationRegistry = verificationRegistry || createDefaultVerificationRegistry()
|
|
28
|
-
this.noPostCheckConditions = noPostCheckConditions ?? false
|
|
35
|
+
this.events = options?.eventEmitter || deploymentEvents
|
|
36
|
+
this.verificationRegistry = options?.verificationRegistry || createDefaultVerificationRegistry()
|
|
37
|
+
this.noPostCheckConditions = options?.noPostCheckConditions ?? false
|
|
38
|
+
this.allowMultipleNicksMethodTests = options?.allowMultipleNicksMethodTests ?? false
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
/**
|
|
@@ -781,6 +791,18 @@ export class ExecutionEngine {
|
|
|
781
791
|
break
|
|
782
792
|
}
|
|
783
793
|
case 'test-nicks-method': {
|
|
794
|
+
if (this.nicksMethodTested && !this.allowMultipleNicksMethodTests) {
|
|
795
|
+
try {
|
|
796
|
+
if (context.getOutput(`${action.name}.success`) === true) {
|
|
797
|
+
// Return previous result
|
|
798
|
+
break
|
|
799
|
+
}
|
|
800
|
+
} catch (e) {
|
|
801
|
+
throw new Error(`Nick's method test already performed this run`)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
this.nicksMethodTested = true
|
|
805
|
+
|
|
784
806
|
// Default bytecode if none provided
|
|
785
807
|
const defaultBytecode = '0x608060405234801561001057600080fd5b5061013d806100206000396000f3fe60806040526004361061001e5760003560e01c80639c4ae2d014610023575b600080fd5b6100cb6004803603604081101561003957600080fd5b81019060208101813564010000000081111561005457600080fd5b82018360208201111561006657600080fd5b8035906020019184600183028401116401000000008311171561008857600080fd5b91908080601f01602080910402602001604051908101604052809392919081815260200183838082843760009201919091525092955050913592506100cd915050565b005b60008183516020850134f56040805173ffffffffffffffffffffffffffffffffffffffff83168152905191925081900360200190a050505056fea264697066735822122033609f614f03931b92d88c309d698449bb77efcd517328d341fa4f923c5d8c7964736f6c63430007060033'
|
|
786
808
|
|
package/src/lib/deployer.ts
CHANGED
|
@@ -154,10 +154,16 @@ export class Deployer {
|
|
|
154
154
|
|
|
155
155
|
// 4. Execute the plan.
|
|
156
156
|
const verificationRegistry = createDefaultVerificationRegistry(this.options.etherscanApiKey)
|
|
157
|
-
const engine = new ExecutionEngine(this.loader.templates,
|
|
157
|
+
const engine = new ExecutionEngine(this.loader.templates, {
|
|
158
|
+
eventEmitter: this.events,
|
|
159
|
+
verificationRegistry,
|
|
160
|
+
noPostCheckConditions: this.noPostCheckConditions
|
|
161
|
+
})
|
|
158
162
|
|
|
159
163
|
// Track if any jobs have failed
|
|
160
164
|
let hasFailures = false
|
|
165
|
+
// Emit signer info once per network (chainId)
|
|
166
|
+
const signerInfoPrintedForChain = new Set<number>()
|
|
161
167
|
|
|
162
168
|
for (const network of targetNetworks) {
|
|
163
169
|
this.events.emitEvent({
|
|
@@ -168,7 +174,6 @@ export class Deployer {
|
|
|
168
174
|
chainId: network.chainId
|
|
169
175
|
}
|
|
170
176
|
})
|
|
171
|
-
|
|
172
177
|
for (const jobName of jobsToRun) {
|
|
173
178
|
const job = this.loader.jobs.get(jobName)!
|
|
174
179
|
|
|
@@ -204,6 +209,46 @@ export class Deployer {
|
|
|
204
209
|
(context as unknown as { setJobConstants: (constants: unknown) => void }).setJobConstants(job.constants)
|
|
205
210
|
}
|
|
206
211
|
|
|
212
|
+
// Emit signer info once per network using the first job's context
|
|
213
|
+
if (!signerInfoPrintedForChain.has(network.chainId)) {
|
|
214
|
+
try {
|
|
215
|
+
const getSignerFn = (context as unknown as {
|
|
216
|
+
getResolvedSigner?: () => Promise<{ getAddress: () => Promise<string> }>
|
|
217
|
+
signer?: { getAddress: () => Promise<string> }
|
|
218
|
+
}).getResolvedSigner
|
|
219
|
+
const signer = getSignerFn
|
|
220
|
+
? await getSignerFn.call(context)
|
|
221
|
+
: (context as unknown as { signer?: { getAddress: () => Promise<string> } }).signer
|
|
222
|
+
if (signer && typeof signer.getAddress === 'function') {
|
|
223
|
+
const address = await signer.getAddress()
|
|
224
|
+
// provider may not exist on mocked contexts; guard for it
|
|
225
|
+
const provider = (context as unknown as {
|
|
226
|
+
provider?: { getBalance: (addr: string) => Promise<bigint | number | { toString: () => string }> }
|
|
227
|
+
}).provider
|
|
228
|
+
if (provider && typeof provider.getBalance === 'function') {
|
|
229
|
+
const balanceBn = await provider.getBalance(address)
|
|
230
|
+
const balanceWei = balanceBn.toString()
|
|
231
|
+
const balanceEth = (Number(balanceBn) / 1e18).toString()
|
|
232
|
+
this.events.emitEvent({
|
|
233
|
+
type: 'network_signer_info',
|
|
234
|
+
level: 'info',
|
|
235
|
+
data: {
|
|
236
|
+
networkName: network.name,
|
|
237
|
+
chainId: network.chainId,
|
|
238
|
+
address,
|
|
239
|
+
balanceWei,
|
|
240
|
+
balance: balanceEth
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
// ignore non-fatal signer info errors
|
|
247
|
+
} finally {
|
|
248
|
+
signerInfoPrintedForChain.add(network.chainId)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
207
252
|
// Populate context with outputs from previously executed dependent jobs
|
|
208
253
|
this.populateContextWithDependentJobOutputs(job, context, network)
|
|
209
254
|
|
|
@@ -275,12 +320,32 @@ export class Deployer {
|
|
|
275
320
|
// Check if any jobs failed and exit with error if so
|
|
276
321
|
if (hasFailures) {
|
|
277
322
|
const error = new Error('One or more jobs failed during execution')
|
|
323
|
+
|
|
324
|
+
// Build a flat list of failed jobs with network context and error messages
|
|
325
|
+
const failedJobs: Array<{ jobName: string; networkName: string; chainId: number; error: string }> = []
|
|
326
|
+
for (const [, result] of this.results) {
|
|
327
|
+
const job = result.job
|
|
328
|
+
for (const [chainId, netResult] of result.outputs) {
|
|
329
|
+
if (netResult.status === 'error') {
|
|
330
|
+
// Resolve network name from configured networks (fallback to chainId if missing)
|
|
331
|
+
const network = this.options.networks.find(n => n.chainId === chainId)
|
|
332
|
+
failedJobs.push({
|
|
333
|
+
jobName: job.name,
|
|
334
|
+
networkName: network?.name || `chain-${chainId}`,
|
|
335
|
+
chainId,
|
|
336
|
+
error: String(netResult.data)
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
278
342
|
this.events.emitEvent({
|
|
279
343
|
type: 'deployment_failed',
|
|
280
344
|
level: 'error',
|
|
281
345
|
data: {
|
|
282
346
|
error: error.message,
|
|
283
|
-
stack: error.stack
|
|
347
|
+
stack: error.stack,
|
|
348
|
+
failedJobs
|
|
284
349
|
}
|
|
285
350
|
})
|
|
286
351
|
throw error
|
|
@@ -48,7 +48,7 @@ export class CLIEventAdapter {
|
|
|
48
48
|
const level0Events = new Set([
|
|
49
49
|
'deployment_started', 'deployment_completed', 'deployment_failed',
|
|
50
50
|
'job_started', 'job_completed', 'job_skipped', 'job_execution_failed',
|
|
51
|
-
'network_started',
|
|
51
|
+
'network_started', 'network_signer_info',
|
|
52
52
|
'duplicate_artifact_warning', 'missing_network_config_warning',
|
|
53
53
|
'unhandled_rejection', 'uncaught_exception', 'cli_error',
|
|
54
54
|
'verification_failed'
|
|
@@ -115,6 +115,11 @@ export class CLIEventAdapter {
|
|
|
115
115
|
console.log(chalk.cyan.bold(`\nNetwork: ${event.data.networkName} (ChainID: ${event.data.chainId})`))
|
|
116
116
|
break
|
|
117
117
|
|
|
118
|
+
case 'network_signer_info':
|
|
119
|
+
console.log(chalk.gray(` Sender: ${event.data.address}`))
|
|
120
|
+
console.log(chalk.gray(` Balance: ${event.data.balance} ETH (${event.data.balanceWei} wei)`))
|
|
121
|
+
break
|
|
122
|
+
|
|
118
123
|
case 'job_started':
|
|
119
124
|
console.log(chalk.cyan.bold(`\nš Starting job: ${event.data.jobName} (v${event.data.jobVersion})`))
|
|
120
125
|
break
|
|
@@ -197,10 +202,21 @@ export class CLIEventAdapter {
|
|
|
197
202
|
|
|
198
203
|
case 'deployment_failed':
|
|
199
204
|
console.error(chalk.red.bold('\nš„ DEPLOYMENT FAILED!'))
|
|
200
|
-
if
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
console.error(chalk.red(
|
|
205
|
+
// Show concise failed jobs table if present
|
|
206
|
+
const failedJobs = (event as any).data?.failedJobs as Array<{ jobName: string; networkName: string; chainId: number; error: string }> | undefined
|
|
207
|
+
if (Array.isArray(failedJobs) && failedJobs.length > 0) {
|
|
208
|
+
console.error(chalk.red(' ā Failed jobs:'))
|
|
209
|
+
for (const f of failedJobs) {
|
|
210
|
+
const where = `${f.networkName} (ChainID: ${f.chainId})`
|
|
211
|
+
console.error(chalk.red(` - ${f.jobName} on ${where}`))
|
|
212
|
+
console.error(chalk.red(` Error: ${f.error}`))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Always print the top-level error and stack last
|
|
216
|
+
if ((event as any).data?.stack) {
|
|
217
|
+
console.error(chalk.red((event as any).data.stack))
|
|
218
|
+
} else if ((event as any).data?.error) {
|
|
219
|
+
console.error(chalk.red((event as any).data.error))
|
|
204
220
|
}
|
|
205
221
|
break
|
|
206
222
|
|
package/src/lib/events/types.ts
CHANGED
|
@@ -29,6 +29,12 @@ export interface DeploymentFailedEvent extends BaseEvent {
|
|
|
29
29
|
data: {
|
|
30
30
|
error: string
|
|
31
31
|
stack?: string
|
|
32
|
+
failedJobs?: Array<{
|
|
33
|
+
jobName: string
|
|
34
|
+
networkName: string
|
|
35
|
+
chainId: number
|
|
36
|
+
error: string
|
|
37
|
+
}>
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -312,6 +318,22 @@ export interface NetworkStartedEvent extends BaseEvent {
|
|
|
312
318
|
}
|
|
313
319
|
}
|
|
314
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Emitted right after a network run starts to inform which address will be used
|
|
323
|
+
* to send transactions and its current balance.
|
|
324
|
+
*/
|
|
325
|
+
export interface NetworkSignerInfoEvent extends BaseEvent {
|
|
326
|
+
type: 'network_signer_info'
|
|
327
|
+
level: 'info'
|
|
328
|
+
data: {
|
|
329
|
+
networkName: string
|
|
330
|
+
chainId: number
|
|
331
|
+
address: string
|
|
332
|
+
balanceWei: string
|
|
333
|
+
balance: string // formatted in ETH
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
315
337
|
// Process error events
|
|
316
338
|
export interface UnhandledRejectionEvent extends BaseEvent {
|
|
317
339
|
type: 'unhandled_rejection'
|
|
@@ -457,6 +479,7 @@ export type DeploymentEvent =
|
|
|
457
479
|
| ContextDisposalWarningEvent
|
|
458
480
|
| DeprecatedJobsSkippedEvent
|
|
459
481
|
| NetworkStartedEvent
|
|
482
|
+
| NetworkSignerInfoEvent
|
|
460
483
|
| UnhandledRejectionEvent
|
|
461
484
|
| UncaughtExceptionEvent
|
|
462
485
|
| CLIErrorEvent
|
|
@@ -37,7 +37,8 @@ function resolveRpcUrlTokens(rpcUrl: string): string {
|
|
|
37
37
|
}
|
|
38
38
|
const value = process.env[varName]
|
|
39
39
|
if (typeof value === 'undefined') {
|
|
40
|
-
|
|
40
|
+
// Default to empty string if missing
|
|
41
|
+
return ''
|
|
41
42
|
}
|
|
42
43
|
return value
|
|
43
44
|
})
|