@0xsequence/catapult 1.3.16 → 1.4.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 +250 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +12 -0
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/provenance.d.ts +3 -0
- package/dist/commands/provenance.d.ts.map +1 -0
- package/dist/commands/provenance.js +138 -0
- package/dist/commands/provenance.js.map +1 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +7 -4
- package/dist/commands/run.js.map +1 -1
- package/dist/lib/__tests__/deployer.spec.js +118 -1
- package/dist/lib/__tests__/deployer.spec.js.map +1 -1
- package/dist/lib/__tests__/network-utils.spec.js +53 -8
- package/dist/lib/__tests__/network-utils.spec.js.map +1 -1
- package/dist/lib/__tests__/provenance.spec.d.ts +2 -0
- package/dist/lib/__tests__/provenance.spec.d.ts.map +1 -0
- package/dist/lib/__tests__/provenance.spec.js +205 -0
- package/dist/lib/__tests__/provenance.spec.js.map +1 -0
- package/dist/lib/contracts/__tests__/repository.spec.js +243 -0
- package/dist/lib/contracts/__tests__/repository.spec.js.map +1 -1
- package/dist/lib/contracts/repository.d.ts +9 -1
- package/dist/lib/contracts/repository.d.ts.map +1 -1
- package/dist/lib/contracts/repository.js +93 -7
- package/dist/lib/contracts/repository.js.map +1 -1
- package/dist/lib/core/__tests__/assert-action.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/assert-action.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/assert-action.spec.js +377 -0
- package/dist/lib/core/__tests__/assert-action.spec.js.map +1 -0
- package/dist/lib/core/__tests__/engine.spec.js +80 -0
- package/dist/lib/core/__tests__/engine.spec.js.map +1 -1
- package/dist/lib/core/__tests__/loader.spec.js +29 -0
- package/dist/lib/core/__tests__/loader.spec.js.map +1 -1
- package/dist/lib/core/__tests__/resolver.spec.js +383 -0
- package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +33 -0
- package/dist/lib/core/engine.js.map +1 -1
- package/dist/lib/core/loader.d.ts +1 -0
- package/dist/lib/core/loader.d.ts.map +1 -1
- package/dist/lib/core/loader.js +6 -1
- package/dist/lib/core/loader.js.map +1 -1
- package/dist/lib/core/resolver.d.ts +2 -0
- package/dist/lib/core/resolver.d.ts.map +1 -1
- package/dist/lib/core/resolver.js +89 -0
- package/dist/lib/core/resolver.js.map +1 -1
- package/dist/lib/deployer.d.ts.map +1 -1
- package/dist/lib/deployer.js +21 -4
- package/dist/lib/deployer.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/parsers/__tests__/job.spec.js +77 -0
- package/dist/lib/parsers/__tests__/job.spec.js.map +1 -1
- package/dist/lib/parsers/__tests__/source.spec.d.ts +2 -0
- package/dist/lib/parsers/__tests__/source.spec.d.ts.map +1 -0
- package/dist/lib/parsers/__tests__/source.spec.js +121 -0
- package/dist/lib/parsers/__tests__/source.spec.js.map +1 -0
- package/dist/lib/parsers/index.d.ts +1 -0
- package/dist/lib/parsers/index.d.ts.map +1 -1
- package/dist/lib/parsers/index.js +1 -0
- package/dist/lib/parsers/index.js.map +1 -1
- package/dist/lib/parsers/job.d.ts.map +1 -1
- package/dist/lib/parsers/job.js +11 -0
- package/dist/lib/parsers/job.js.map +1 -1
- package/dist/lib/parsers/source.d.ts +4 -0
- package/dist/lib/parsers/source.d.ts.map +1 -0
- package/dist/lib/parsers/source.js +107 -0
- package/dist/lib/parsers/source.js.map +1 -0
- package/dist/lib/provenance.d.ts +34 -0
- package/dist/lib/provenance.d.ts.map +1 -0
- package/dist/lib/provenance.js +645 -0
- package/dist/lib/provenance.js.map +1 -0
- package/dist/lib/types/actions.d.ts +18 -2
- package/dist/lib/types/actions.d.ts.map +1 -1
- package/dist/lib/types/actions.js +1 -0
- package/dist/lib/types/actions.js.map +1 -1
- package/dist/lib/types/contracts.d.ts +3 -0
- package/dist/lib/types/contracts.d.ts.map +1 -1
- package/dist/lib/types/definitions.d.ts +1 -0
- package/dist/lib/types/definitions.d.ts.map +1 -1
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/source.d.ts +24 -0
- package/dist/lib/types/source.d.ts.map +1 -0
- package/dist/lib/types/source.js +3 -0
- package/dist/lib/types/source.js.map +1 -0
- package/dist/lib/types/values.d.ts +33 -1
- package/dist/lib/types/values.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -2
- package/src/commands/index.ts +2 -1
- package/src/commands/list.ts +14 -1
- package/src/commands/provenance.ts +120 -0
- package/src/commands/run.ts +11 -6
- package/src/lib/__tests__/deployer.spec.ts +177 -1
- package/src/lib/__tests__/network-utils.spec.ts +63 -14
- package/src/lib/__tests__/provenance.spec.ts +208 -0
- package/src/lib/contracts/__tests__/repository.spec.ts +270 -2
- package/src/lib/contracts/repository.ts +112 -14
- package/src/lib/core/__tests__/assert-action.spec.ts +474 -0
- package/src/lib/core/__tests__/engine.spec.ts +116 -0
- package/src/lib/core/__tests__/loader.spec.ts +34 -1
- package/src/lib/core/__tests__/resolver.spec.ts +444 -1
- package/src/lib/core/engine.ts +52 -0
- package/src/lib/core/loader.ts +8 -2
- package/src/lib/core/resolver.ts +116 -0
- package/src/lib/deployer.ts +28 -4
- package/src/lib/index.ts +4 -1
- package/src/lib/parsers/__tests__/job.spec.ts +81 -0
- package/src/lib/parsers/__tests__/source.spec.ts +134 -0
- package/src/lib/parsers/index.ts +1 -0
- package/src/lib/parsers/job.ts +14 -2
- package/src/lib/parsers/source.ts +129 -0
- package/src/lib/provenance.ts +785 -0
- package/src/lib/types/actions.ts +22 -1
- package/src/lib/types/contracts.ts +4 -1
- package/src/lib/types/definitions.ts +7 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/types/source.ts +26 -0
- package/src/lib/types/values.ts +71 -0
|
@@ -1,7 +1,64 @@
|
|
|
1
1
|
import * as fs from 'fs/promises'
|
|
2
2
|
import * as path from 'path'
|
|
3
3
|
import { ContractRepository } from '../repository'
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
function buildInfoContent(): string {
|
|
6
|
+
return JSON.stringify({
|
|
7
|
+
_format: 'hh-sol-build-info-1',
|
|
8
|
+
id: 'test-build-id',
|
|
9
|
+
solcVersion: '0.8.0',
|
|
10
|
+
solcLongVersion: '0.8.0+commit.c7dfd78e',
|
|
11
|
+
input: {
|
|
12
|
+
language: 'Solidity',
|
|
13
|
+
sources: {
|
|
14
|
+
'src/Stage1Module.sol': {
|
|
15
|
+
content: 'contract Stage1Module {}'
|
|
16
|
+
},
|
|
17
|
+
'src/Stage2Module.sol': {
|
|
18
|
+
content: 'contract Stage2Module {}'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
settings: {
|
|
22
|
+
outputSelection: {
|
|
23
|
+
'*': {
|
|
24
|
+
'*': ['abi', 'evm.bytecode', 'evm.deployedBytecode']
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
output: {
|
|
30
|
+
contracts: {
|
|
31
|
+
'src/Stage1Module.sol': {
|
|
32
|
+
Stage1Module: {
|
|
33
|
+
abi: [],
|
|
34
|
+
evm: {
|
|
35
|
+
bytecode: {
|
|
36
|
+
object: '0x608060405234801561001057600080fd5b50111111'
|
|
37
|
+
},
|
|
38
|
+
deployedBytecode: {
|
|
39
|
+
object: '0x608060405234801561001057600080fd5b50111112'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
'src/Stage2Module.sol': {
|
|
45
|
+
Stage2Module: {
|
|
46
|
+
abi: [],
|
|
47
|
+
evm: {
|
|
48
|
+
bytecode: {
|
|
49
|
+
object: '0x608060405234801561001057600080fd5b50222222'
|
|
50
|
+
},
|
|
51
|
+
deployedBytecode: {
|
|
52
|
+
object: '0x608060405234801561001057600080fd5b50222223'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
sources: {}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
5
62
|
|
|
6
63
|
describe('ContractRepository', () => {
|
|
7
64
|
let repository: ContractRepository
|
|
@@ -111,6 +168,217 @@ describe('ContractRepository', () => {
|
|
|
111
168
|
expect(contract._sources.has(buildInfoPath)).toBe(true)
|
|
112
169
|
})
|
|
113
170
|
|
|
171
|
+
it('should attach source provenance from build-info sidecars', async () => {
|
|
172
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
173
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
174
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
175
|
+
const sourcePath = path.join(buildInfoDir, 'source.yaml')
|
|
176
|
+
|
|
177
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
178
|
+
await fs.writeFile(sourcePath, `
|
|
179
|
+
type: source
|
|
180
|
+
build_info:
|
|
181
|
+
"./stage1.json":
|
|
182
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
183
|
+
ref: "v3.0.0-rc.5"
|
|
184
|
+
commit: "0d9061f229da73edae890e6fdd1fbf753028df6d"
|
|
185
|
+
build: "forge build --build-info"
|
|
186
|
+
contracts:
|
|
187
|
+
"src/Stage1Module.sol:Stage1Module":
|
|
188
|
+
ref: "stage1-special"
|
|
189
|
+
`)
|
|
190
|
+
|
|
191
|
+
await repository.loadFrom(tempDir)
|
|
192
|
+
|
|
193
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
194
|
+
const stage2 = repository.lookup(`${buildInfoPath}:Stage2Module`)
|
|
195
|
+
|
|
196
|
+
expect(stage1).not.toBeNull()
|
|
197
|
+
expect(stage2).not.toBeNull()
|
|
198
|
+
|
|
199
|
+
expect(stage1!.sourceProvenance).toMatchObject({
|
|
200
|
+
repo: 'https://github.com/0xsequence/wallet-contracts-v3',
|
|
201
|
+
ref: 'stage1-special',
|
|
202
|
+
commit: '0d9061f229da73edae890e6fdd1fbf753028df6d',
|
|
203
|
+
build: 'forge build --build-info',
|
|
204
|
+
sourceDocumentPath: sourcePath,
|
|
205
|
+
buildInfoPath
|
|
206
|
+
})
|
|
207
|
+
expect(stage2!.sourceProvenance).toMatchObject({
|
|
208
|
+
repo: 'https://github.com/0xsequence/wallet-contracts-v3',
|
|
209
|
+
ref: 'v3.0.0-rc.5',
|
|
210
|
+
commit: '0d9061f229da73edae890e6fdd1fbf753028df6d'
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(stage1!._sourceProvenance?.get(buildInfoPath)?.ref).toBe('stage1-special')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should skip source sidecars that point to missing build-info files', async () => {
|
|
217
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
218
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
219
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
220
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
221
|
+
type: source
|
|
222
|
+
build_info:
|
|
223
|
+
"./missing.json":
|
|
224
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
225
|
+
`)
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await expect(repository.loadFrom(tempDir)).resolves.toBeUndefined()
|
|
229
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('does not exist'))
|
|
230
|
+
} finally {
|
|
231
|
+
warnSpy.mockRestore()
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should skip malformed source sidecars without blocking build-info loading', async () => {
|
|
236
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
237
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
238
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
239
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
240
|
+
|
|
241
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
242
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
243
|
+
type: source
|
|
244
|
+
build_info:
|
|
245
|
+
"./stage1.json":
|
|
246
|
+
ref: "v3.0.0-rc.5"
|
|
247
|
+
`)
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await repository.loadFrom(tempDir)
|
|
251
|
+
} finally {
|
|
252
|
+
warnSpy.mockRestore()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
256
|
+
expect(stage1).not.toBeNull()
|
|
257
|
+
expect(stage1!.sourceProvenance).toBeUndefined()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should keep valid source sidecar entries when sibling entries are invalid', async () => {
|
|
261
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
262
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
263
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
264
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
265
|
+
|
|
266
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
267
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
268
|
+
type: source
|
|
269
|
+
build_info:
|
|
270
|
+
"./stage1.json":
|
|
271
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
272
|
+
commit: "0d9061f229da73edae890e6fdd1fbf753028df6d"
|
|
273
|
+
"./bad.json":
|
|
274
|
+
typo_field: true
|
|
275
|
+
`)
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
await repository.loadFrom(tempDir)
|
|
279
|
+
|
|
280
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
281
|
+
expect(stage1).not.toBeNull()
|
|
282
|
+
expect(stage1!.sourceProvenance).toMatchObject({
|
|
283
|
+
repo: 'https://github.com/0xsequence/wallet-contracts-v3',
|
|
284
|
+
commit: '0d9061f229da73edae890e6fdd1fbf753028df6d'
|
|
285
|
+
})
|
|
286
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('typo_field is not supported'))
|
|
287
|
+
} finally {
|
|
288
|
+
warnSpy.mockRestore()
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should skip source sidecar entries that do not point to build-info JSON', async () => {
|
|
293
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
294
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
295
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
296
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
297
|
+
|
|
298
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
299
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
300
|
+
type: source
|
|
301
|
+
build_info:
|
|
302
|
+
"./stage1.txt":
|
|
303
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
304
|
+
`)
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
await repository.loadFrom(tempDir)
|
|
308
|
+
|
|
309
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
310
|
+
expect(stage1).not.toBeNull()
|
|
311
|
+
expect(stage1!.sourceProvenance).toBeUndefined()
|
|
312
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('does not point to a build-info JSON file'))
|
|
313
|
+
} finally {
|
|
314
|
+
warnSpy.mockRestore()
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('should skip duplicate source provenance entries for the same build-info file', async () => {
|
|
319
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
320
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
321
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
322
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
323
|
+
|
|
324
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
325
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
326
|
+
type: source
|
|
327
|
+
build_info:
|
|
328
|
+
"./stage1.json":
|
|
329
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3-a"
|
|
330
|
+
`)
|
|
331
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yml'), `
|
|
332
|
+
type: source
|
|
333
|
+
build_info:
|
|
334
|
+
"./stage1.json":
|
|
335
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3-b"
|
|
336
|
+
`)
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
await repository.loadFrom(tempDir)
|
|
340
|
+
|
|
341
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
342
|
+
expect(stage1).not.toBeNull()
|
|
343
|
+
expect(stage1!._sourceProvenance?.size).toBe(1)
|
|
344
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('duplicate provenance'))
|
|
345
|
+
} finally {
|
|
346
|
+
warnSpy.mockRestore()
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should select preferred source provenance deterministically for duplicate bytecode', async () => {
|
|
351
|
+
const olderBuildInfoDir = path.join(tempDir, 'build-info', 'b-release')
|
|
352
|
+
const newerBuildInfoDir = path.join(tempDir, 'build-info', 'a-release')
|
|
353
|
+
await fs.mkdir(olderBuildInfoDir, { recursive: true })
|
|
354
|
+
await fs.mkdir(newerBuildInfoDir, { recursive: true })
|
|
355
|
+
|
|
356
|
+
const olderBuildInfoPath = path.join(olderBuildInfoDir, 'stage1.json')
|
|
357
|
+
const newerBuildInfoPath = path.join(newerBuildInfoDir, 'stage1.json')
|
|
358
|
+
await fs.writeFile(olderBuildInfoPath, buildInfoContent())
|
|
359
|
+
await fs.writeFile(newerBuildInfoPath, buildInfoContent())
|
|
360
|
+
|
|
361
|
+
await fs.writeFile(path.join(olderBuildInfoDir, 'source.yaml'), `
|
|
362
|
+
type: source
|
|
363
|
+
build_info:
|
|
364
|
+
"./stage1.json":
|
|
365
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3-old"
|
|
366
|
+
`)
|
|
367
|
+
await fs.writeFile(path.join(newerBuildInfoDir, 'source.yaml'), `
|
|
368
|
+
type: source
|
|
369
|
+
build_info:
|
|
370
|
+
"./stage1.json":
|
|
371
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3-new"
|
|
372
|
+
`)
|
|
373
|
+
|
|
374
|
+
await repository.loadFrom(tempDir)
|
|
375
|
+
|
|
376
|
+
const stage1 = repository.lookup(`${newerBuildInfoPath}:Stage1Module`)
|
|
377
|
+
expect(stage1).not.toBeNull()
|
|
378
|
+
expect(stage1!._sourceProvenance?.size).toBe(2)
|
|
379
|
+
expect(stage1!.sourceProvenance?.repo).toBe('https://github.com/0xsequence/wallet-contracts-v3-new')
|
|
380
|
+
})
|
|
381
|
+
|
|
114
382
|
it('should hydrate contracts from multiple source files', async () => {
|
|
115
383
|
// Create a basic artifact file (minimal info, will be hydrated by build-info)
|
|
116
384
|
const artifactContent = JSON.stringify({
|
|
@@ -341,4 +609,4 @@ describe('ContractRepository', () => {
|
|
|
341
609
|
expect(contracts).toHaveLength(0)
|
|
342
610
|
})
|
|
343
611
|
})
|
|
344
|
-
})
|
|
612
|
+
})
|
|
@@ -2,23 +2,26 @@ import * as fs from 'fs/promises'
|
|
|
2
2
|
import * as path from 'path'
|
|
3
3
|
import { createHash } from 'crypto'
|
|
4
4
|
import { Contract } from '../types/contracts'
|
|
5
|
+
import { BuildInfoSourceProvenance, SourceProvenance } from '../types/source'
|
|
5
6
|
import { parseArtifact } from '../parsers/artifact'
|
|
6
7
|
import { parseBuildInfo, isBuildInfoFile } from '../parsers/buildinfo'
|
|
8
|
+
import { mergeSourceProvenance, parseSourceDocument } from '../parsers/source'
|
|
7
9
|
|
|
8
10
|
export class ContractRepository {
|
|
9
11
|
private contracts: Map<string, Contract> = new Map()
|
|
10
12
|
private referenceMap: Map<string, string[]> = new Map()
|
|
11
13
|
private ambiguousReferences: Set<string> = new Set()
|
|
14
|
+
private sourceProvenanceByBuildInfoPath: Map<string, BuildInfoSourceProvenance> = new Map()
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Main entry point that orchestrates the discovery and hydration process
|
|
15
18
|
*/
|
|
16
19
|
public async loadFrom(projectRoot: string): Promise<void> {
|
|
17
|
-
|
|
18
|
-
const files = await this.findContractFiles(projectRoot)
|
|
20
|
+
const files = await this.findProjectFiles(projectRoot)
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
await this.loadSourceProvenanceFiles(files.sourceFiles)
|
|
23
|
+
|
|
24
|
+
for (const filePath of files.contractFiles) {
|
|
22
25
|
try {
|
|
23
26
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
24
27
|
await this.parseAndHydrateFromFile(content, filePath)
|
|
@@ -27,7 +30,6 @@ export class ContractRepository {
|
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
// Step 3: Build reference maps and identify ambiguous references
|
|
31
33
|
this.disambiguateReferences()
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -40,6 +42,7 @@ export class ContractRepository {
|
|
|
40
42
|
const extractedContracts = parseBuildInfo(content, filePath)
|
|
41
43
|
if (extractedContracts) {
|
|
42
44
|
for (const extracted of extractedContracts) {
|
|
45
|
+
const sourceProvenance = this.getSourceProvenance(filePath, extracted.fullyQualifiedName)
|
|
43
46
|
this.hydrateContract({
|
|
44
47
|
creationCode: extracted.bytecode,
|
|
45
48
|
runtimeBytecode: extracted.deployedBytecode,
|
|
@@ -49,6 +52,7 @@ export class ContractRepository {
|
|
|
49
52
|
source: extracted.source,
|
|
50
53
|
compiler: extracted.compiler,
|
|
51
54
|
buildInfoId: extracted.buildInfoId,
|
|
55
|
+
sourceProvenance,
|
|
52
56
|
}, filePath)
|
|
53
57
|
}
|
|
54
58
|
return
|
|
@@ -82,6 +86,7 @@ export class ContractRepository {
|
|
|
82
86
|
source?: string
|
|
83
87
|
compiler?: any
|
|
84
88
|
buildInfoId?: string
|
|
89
|
+
sourceProvenance?: SourceProvenance
|
|
85
90
|
}, sourceFilePath: string): void {
|
|
86
91
|
// Validate that we have creation code for hashing (but allow empty string)
|
|
87
92
|
if (data.creationCode === null || data.creationCode === undefined) {
|
|
@@ -97,7 +102,8 @@ export class ContractRepository {
|
|
|
97
102
|
contract = {
|
|
98
103
|
uniqueHash,
|
|
99
104
|
creationCode: data.creationCode,
|
|
100
|
-
_sources: new Set<string>()
|
|
105
|
+
_sources: new Set<string>(),
|
|
106
|
+
_sourceProvenance: new Map<string, SourceProvenance>()
|
|
101
107
|
}
|
|
102
108
|
this.contracts.set(uniqueHash, contract)
|
|
103
109
|
}
|
|
@@ -130,6 +136,28 @@ export class ContractRepository {
|
|
|
130
136
|
if (data.buildInfoId && !contract.buildInfoId) {
|
|
131
137
|
contract.buildInfoId = data.buildInfoId
|
|
132
138
|
}
|
|
139
|
+
if (data.sourceProvenance) {
|
|
140
|
+
if (!contract._sourceProvenance) {
|
|
141
|
+
contract._sourceProvenance = new Map<string, SourceProvenance>()
|
|
142
|
+
}
|
|
143
|
+
contract._sourceProvenance.set(sourceFilePath, data.sourceProvenance)
|
|
144
|
+
|
|
145
|
+
contract.sourceProvenance = this.selectPreferredSourceProvenance(contract._sourceProvenance)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private getSourceProvenance(buildInfoPath: string, fullyQualifiedName: string): SourceProvenance | undefined {
|
|
150
|
+
const provenance = this.sourceProvenanceByBuildInfoPath.get(buildInfoPath)
|
|
151
|
+
if (!provenance) {
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return mergeSourceProvenance(provenance, provenance.contracts?.[fullyQualifiedName])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private selectPreferredSourceProvenance(sourceProvenance: Map<string, SourceProvenance>): SourceProvenance | undefined {
|
|
159
|
+
const firstEntry = Array.from(sourceProvenance.entries()).sort(([a], [b]) => a.localeCompare(b))[0]
|
|
160
|
+
return firstEntry?.[1]
|
|
133
161
|
}
|
|
134
162
|
|
|
135
163
|
/**
|
|
@@ -269,6 +297,7 @@ export class ContractRepository {
|
|
|
269
297
|
source?: string
|
|
270
298
|
compiler?: any
|
|
271
299
|
buildInfoId?: string
|
|
300
|
+
sourceProvenance?: SourceProvenance
|
|
272
301
|
_path: string
|
|
273
302
|
_hash: string
|
|
274
303
|
}): void {
|
|
@@ -281,17 +310,21 @@ export class ContractRepository {
|
|
|
281
310
|
source: contractData.source,
|
|
282
311
|
compiler: contractData.compiler,
|
|
283
312
|
buildInfoId: contractData.buildInfoId,
|
|
313
|
+
sourceProvenance: contractData.sourceProvenance,
|
|
284
314
|
}, contractData._path)
|
|
285
315
|
|
|
286
316
|
// For testing, immediately disambiguate references after adding
|
|
287
317
|
this.disambiguateReferences()
|
|
288
318
|
}
|
|
289
319
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
320
|
+
private async findProjectFiles(
|
|
321
|
+
dir: string,
|
|
322
|
+
ignoreDirs: Set<string> = new Set(['node_modules', 'dist', '.git', '.idea', '.vscode'])
|
|
323
|
+
): Promise<{ contractFiles: string[]; sourceFiles: string[] }> {
|
|
324
|
+
const results: { contractFiles: string[]; sourceFiles: string[] } = {
|
|
325
|
+
contractFiles: [],
|
|
326
|
+
sourceFiles: []
|
|
327
|
+
}
|
|
295
328
|
try {
|
|
296
329
|
const list = await fs.readdir(dir, { withFileTypes: true })
|
|
297
330
|
|
|
@@ -299,10 +332,14 @@ export class ContractRepository {
|
|
|
299
332
|
const fullPath = path.resolve(dir, dirent.name)
|
|
300
333
|
if (dirent.isDirectory()) {
|
|
301
334
|
if (!ignoreDirs.has(dirent.name)) {
|
|
302
|
-
|
|
335
|
+
const childResults = await this.findProjectFiles(fullPath, ignoreDirs)
|
|
336
|
+
results.contractFiles.push(...childResults.contractFiles)
|
|
337
|
+
results.sourceFiles.push(...childResults.sourceFiles)
|
|
303
338
|
}
|
|
304
339
|
} else if (dirent.isFile() && dirent.name.endsWith('.json')) {
|
|
305
|
-
results.push(fullPath)
|
|
340
|
+
results.contractFiles.push(fullPath)
|
|
341
|
+
} else if (dirent.isFile() && (dirent.name === 'source.yaml' || dirent.name === 'source.yml')) {
|
|
342
|
+
results.sourceFiles.push(fullPath)
|
|
306
343
|
}
|
|
307
344
|
}
|
|
308
345
|
} catch (err) {
|
|
@@ -310,4 +347,65 @@ export class ContractRepository {
|
|
|
310
347
|
}
|
|
311
348
|
return results
|
|
312
349
|
}
|
|
313
|
-
|
|
350
|
+
|
|
351
|
+
private async loadSourceProvenanceFiles(sourceFiles: string[]): Promise<void> {
|
|
352
|
+
this.sourceProvenanceByBuildInfoPath.clear()
|
|
353
|
+
|
|
354
|
+
for (const sourceFilePath of sourceFiles) {
|
|
355
|
+
let sourceDocument
|
|
356
|
+
try {
|
|
357
|
+
const content = await fs.readFile(sourceFilePath, 'utf-8')
|
|
358
|
+
sourceDocument = parseSourceDocument(content)
|
|
359
|
+
} catch (error) {
|
|
360
|
+
this.warnSourceProvenance(`Skipping source provenance file ${sourceFilePath}: ${error instanceof Error ? error.message : String(error)}`)
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!sourceDocument) {
|
|
365
|
+
continue
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const warning of sourceDocument.warnings || []) {
|
|
369
|
+
this.warnSourceProvenance(`Skipping source provenance entry ${sourceFilePath}: ${warning}`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const sourceDir = path.dirname(sourceFilePath)
|
|
373
|
+
for (const [buildInfoRef, provenance] of Object.entries(sourceDocument.build_info)) {
|
|
374
|
+
const buildInfoPath = path.resolve(sourceDir, buildInfoRef)
|
|
375
|
+
if (!isBuildInfoFile(buildInfoPath)) {
|
|
376
|
+
this.warnSourceProvenance(`Skipping source provenance entry ${sourceFilePath}: "${buildInfoRef}" does not point to a build-info JSON file.`)
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!await this.pathExists(buildInfoPath)) {
|
|
381
|
+
this.warnSourceProvenance(`Skipping source provenance entry ${sourceFilePath}: build-info file "${buildInfoRef}" does not exist.`)
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (this.sourceProvenanceByBuildInfoPath.has(buildInfoPath)) {
|
|
386
|
+
this.warnSourceProvenance(`Skipping source provenance entry ${sourceFilePath}: duplicate provenance for build-info file "${buildInfoRef}".`)
|
|
387
|
+
continue
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.sourceProvenanceByBuildInfoPath.set(buildInfoPath, {
|
|
391
|
+
...provenance,
|
|
392
|
+
sourceDocumentPath: sourceFilePath,
|
|
393
|
+
buildInfoPath
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async pathExists(filePath: string): Promise<boolean> {
|
|
400
|
+
try {
|
|
401
|
+
await fs.access(filePath)
|
|
402
|
+
return true
|
|
403
|
+
} catch {
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private warnSourceProvenance(message: string): void {
|
|
409
|
+
console.warn(message)
|
|
410
|
+
}
|
|
411
|
+
}
|