@0xsequence/catapult 1.1.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/.eslintrc.json +29 -0
- package/.github/workflows/ci.yml +181 -0
- package/CONCEPT.md +24 -0
- package/README.md +772 -0
- package/contracts/checked-call.huff +65 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +16 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/common.d.ts +11 -0
- package/dist/commands/common.d.ts.map +1 -0
- package/dist/commands/common.js +73 -0
- package/dist/commands/common.js.map +1 -0
- package/dist/commands/dry.d.ts +3 -0
- package/dist/commands/dry.d.ts.map +1 -0
- package/dist/commands/dry.js +171 -0
- package/dist/commands/dry.js.map +1 -0
- package/dist/commands/etherscan.d.ts +3 -0
- package/dist/commands/etherscan.d.ts.map +1 -0
- package/dist/commands/etherscan.js +323 -0
- package/dist/commands/etherscan.js.map +1 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +22 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +259 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +96 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/utils.d.ts +3 -0
- package/dist/commands/utils.d.ts.map +1 -0
- package/dist/commands/utils.js +46 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/__tests__/deployer-events.spec.d.ts +2 -0
- package/dist/lib/__tests__/deployer-events.spec.d.ts.map +1 -0
- package/dist/lib/__tests__/deployer-events.spec.js +260 -0
- package/dist/lib/__tests__/deployer-events.spec.js.map +1 -0
- package/dist/lib/__tests__/deployer.spec.d.ts +2 -0
- package/dist/lib/__tests__/deployer.spec.d.ts.map +1 -0
- package/dist/lib/__tests__/deployer.spec.js +884 -0
- package/dist/lib/__tests__/deployer.spec.js.map +1 -0
- package/dist/lib/__tests__/network-utils.spec.d.ts +2 -0
- package/dist/lib/__tests__/network-utils.spec.d.ts.map +1 -0
- package/dist/lib/__tests__/network-utils.spec.js +140 -0
- package/dist/lib/__tests__/network-utils.spec.js.map +1 -0
- package/dist/lib/contracts/__tests__/repository.spec.d.ts +2 -0
- package/dist/lib/contracts/__tests__/repository.spec.d.ts.map +1 -0
- package/dist/lib/contracts/__tests__/repository.spec.js +321 -0
- package/dist/lib/contracts/__tests__/repository.spec.js.map +1 -0
- package/dist/lib/contracts/repository.d.ts +27 -0
- package/dist/lib/contracts/repository.d.ts.map +1 -0
- package/dist/lib/contracts/repository.js +241 -0
- package/dist/lib/contracts/repository.js.map +1 -0
- package/dist/lib/core/__tests__/engine.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/engine.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/engine.spec.js +1212 -0
- package/dist/lib/core/__tests__/engine.spec.js.map +1 -0
- package/dist/lib/core/__tests__/graph.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/graph.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/graph.spec.js +116 -0
- package/dist/lib/core/__tests__/graph.spec.js.map +1 -0
- package/dist/lib/core/__tests__/json-integration.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/json-integration.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/json-integration.spec.js +300 -0
- package/dist/lib/core/__tests__/json-integration.spec.js.map +1 -0
- package/dist/lib/core/__tests__/loader.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/loader.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/loader.spec.js +288 -0
- package/dist/lib/core/__tests__/loader.spec.js.map +1 -0
- package/dist/lib/core/__tests__/multi-platform-verification.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/multi-platform-verification.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/multi-platform-verification.spec.js +342 -0
- package/dist/lib/core/__tests__/multi-platform-verification.spec.js.map +1 -0
- package/dist/lib/core/__tests__/resolver.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/resolver.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/resolver.spec.js +1367 -0
- package/dist/lib/core/__tests__/resolver.spec.js.map +1 -0
- package/dist/lib/core/__tests__/static-action.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/static-action.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/static-action.spec.js +136 -0
- package/dist/lib/core/__tests__/static-action.spec.js.map +1 -0
- package/dist/lib/core/context.d.ts +29 -0
- package/dist/lib/core/context.d.ts.map +1 -0
- package/dist/lib/core/context.js +88 -0
- package/dist/lib/core/context.js.map +1 -0
- package/dist/lib/core/engine.d.ts +25 -0
- package/dist/lib/core/engine.d.ts.map +1 -0
- package/dist/lib/core/engine.js +1191 -0
- package/dist/lib/core/engine.js.map +1 -0
- package/dist/lib/core/graph.d.ts +18 -0
- package/dist/lib/core/graph.d.ts.map +1 -0
- package/dist/lib/core/graph.js +158 -0
- package/dist/lib/core/graph.js.map +1 -0
- package/dist/lib/core/loader.d.ts +25 -0
- package/dist/lib/core/loader.d.ts.map +1 -0
- package/dist/lib/core/loader.js +248 -0
- package/dist/lib/core/loader.js.map +1 -0
- package/dist/lib/core/resolver.d.ts +20 -0
- package/dist/lib/core/resolver.d.ts.map +1 -0
- package/dist/lib/core/resolver.js +307 -0
- package/dist/lib/core/resolver.js.map +1 -0
- package/dist/lib/deployer.d.ts +39 -0
- package/dist/lib/deployer.d.ts.map +1 -0
- package/dist/lib/deployer.js +533 -0
- package/dist/lib/deployer.js.map +1 -0
- package/dist/lib/events/__tests__/event-system.spec.d.ts +2 -0
- package/dist/lib/events/__tests__/event-system.spec.d.ts.map +1 -0
- package/dist/lib/events/__tests__/event-system.spec.js +256 -0
- package/dist/lib/events/__tests__/event-system.spec.js.map +1 -0
- package/dist/lib/events/cli-adapter.d.ts +13 -0
- package/dist/lib/events/cli-adapter.d.ts.map +1 -0
- package/dist/lib/events/cli-adapter.js +244 -0
- package/dist/lib/events/cli-adapter.js.map +1 -0
- package/dist/lib/events/emitter.d.ts +11 -0
- package/dist/lib/events/emitter.d.ts.map +1 -0
- package/dist/lib/events/emitter.js +29 -0
- package/dist/lib/events/emitter.js.map +1 -0
- package/dist/lib/events/index.d.ts +4 -0
- package/dist/lib/events/index.d.ts.map +1 -0
- package/dist/lib/events/index.js +20 -0
- package/dist/lib/events/index.js.map +1 -0
- package/dist/lib/events/types.d.ts +368 -0
- package/dist/lib/events/types.d.ts.map +1 -0
- package/dist/lib/events/types.js +3 -0
- package/dist/lib/events/types.js.map +1 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +44 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/network-loader.d.ts +3 -0
- package/dist/lib/network-loader.d.ts.map +1 -0
- package/dist/lib/network-loader.js +80 -0
- package/dist/lib/network-loader.js.map +1 -0
- package/dist/lib/network-match.d.ts +3 -0
- package/dist/lib/network-match.d.ts.map +1 -0
- package/dist/lib/network-match.js +62 -0
- package/dist/lib/network-match.js.map +1 -0
- package/dist/lib/network-utils.d.ts +4 -0
- package/dist/lib/network-utils.d.ts.map +1 -0
- package/dist/lib/network-utils.js +39 -0
- package/dist/lib/network-utils.js.map +1 -0
- package/dist/lib/parsers/__tests__/buildinfo.spec.d.ts +2 -0
- package/dist/lib/parsers/__tests__/buildinfo.spec.d.ts.map +1 -0
- package/dist/lib/parsers/__tests__/buildinfo.spec.js +132 -0
- package/dist/lib/parsers/__tests__/buildinfo.spec.js.map +1 -0
- package/dist/lib/parsers/__tests__/job.spec.d.ts +2 -0
- package/dist/lib/parsers/__tests__/job.spec.d.ts.map +1 -0
- package/dist/lib/parsers/__tests__/job.spec.js +318 -0
- package/dist/lib/parsers/__tests__/job.spec.js.map +1 -0
- package/dist/lib/parsers/__tests__/template.spec.d.ts +2 -0
- package/dist/lib/parsers/__tests__/template.spec.d.ts.map +1 -0
- package/dist/lib/parsers/__tests__/template.spec.js +126 -0
- package/dist/lib/parsers/__tests__/template.spec.js.map +1 -0
- package/dist/lib/parsers/artifact/__tests__/artifact.spec.d.ts +2 -0
- package/dist/lib/parsers/artifact/__tests__/artifact.spec.d.ts.map +1 -0
- package/dist/lib/parsers/artifact/__tests__/artifact.spec.js +128 -0
- package/dist/lib/parsers/artifact/__tests__/artifact.spec.js.map +1 -0
- package/dist/lib/parsers/artifact/foundry-1.2.d.ts +3 -0
- package/dist/lib/parsers/artifact/foundry-1.2.d.ts.map +1 -0
- package/dist/lib/parsers/artifact/foundry-1.2.js +82 -0
- package/dist/lib/parsers/artifact/foundry-1.2.js.map +1 -0
- package/dist/lib/parsers/artifact/index.d.ts +3 -0
- package/dist/lib/parsers/artifact/index.d.ts.map +1 -0
- package/dist/lib/parsers/artifact/index.js +17 -0
- package/dist/lib/parsers/artifact/index.js.map +1 -0
- package/dist/lib/parsers/artifact/types.d.ts +3 -0
- package/dist/lib/parsers/artifact/types.d.ts.map +1 -0
- package/dist/lib/parsers/artifact/types.js +3 -0
- package/dist/lib/parsers/artifact/types.js.map +1 -0
- package/dist/lib/parsers/buildinfo.d.ts +5 -0
- package/dist/lib/parsers/buildinfo.d.ts.map +1 -0
- package/dist/lib/parsers/buildinfo.js +85 -0
- package/dist/lib/parsers/buildinfo.js.map +1 -0
- package/dist/lib/parsers/constants.d.ts +4 -0
- package/dist/lib/parsers/constants.d.ts.map +1 -0
- package/dist/lib/parsers/constants.js +45 -0
- package/dist/lib/parsers/constants.js.map +1 -0
- package/dist/lib/parsers/index.d.ts +5 -0
- package/dist/lib/parsers/index.d.ts.map +1 -0
- package/dist/lib/parsers/index.js +21 -0
- package/dist/lib/parsers/index.js.map +1 -0
- package/dist/lib/parsers/job.d.ts +3 -0
- package/dist/lib/parsers/job.d.ts.map +1 -0
- package/dist/lib/parsers/job.js +74 -0
- package/dist/lib/parsers/job.js.map +1 -0
- package/dist/lib/parsers/template.d.ts +3 -0
- package/dist/lib/parsers/template.d.ts.map +1 -0
- package/dist/lib/parsers/template.js +91 -0
- package/dist/lib/parsers/template.js.map +1 -0
- package/dist/lib/std/templates/assured-deployment.yaml +45 -0
- package/dist/lib/std/templates/erc-2470.yaml +67 -0
- package/dist/lib/std/templates/min-balance.yaml +32 -0
- package/dist/lib/std/templates/nano-universal-deployer.yaml +59 -0
- package/dist/lib/std/templates/raw-erc-2470.yaml +59 -0
- package/dist/lib/std/templates/raw-nano-universal-deployer.yaml +51 -0
- package/dist/lib/std/templates/raw-sequence-universal-deployer-2.yaml +48 -0
- package/dist/lib/std/templates/sequence-universal-deployer-2.yaml +57 -0
- package/dist/lib/types/__tests__/json-request-action.spec.d.ts +2 -0
- package/dist/lib/types/__tests__/json-request-action.spec.d.ts.map +1 -0
- package/dist/lib/types/__tests__/json-request-action.spec.js +219 -0
- package/dist/lib/types/__tests__/json-request-action.spec.js.map +1 -0
- package/dist/lib/types/__tests__/read-json-value.spec.d.ts +2 -0
- package/dist/lib/types/__tests__/read-json-value.spec.d.ts.map +1 -0
- package/dist/lib/types/__tests__/read-json-value.spec.js +233 -0
- package/dist/lib/types/__tests__/read-json-value.spec.js.map +1 -0
- package/dist/lib/types/actions.d.ts +74 -0
- package/dist/lib/types/actions.d.ts.map +1 -0
- package/dist/lib/types/actions.js +18 -0
- package/dist/lib/types/actions.js.map +1 -0
- package/dist/lib/types/artifacts.d.ts +15 -0
- package/dist/lib/types/artifacts.d.ts.map +1 -0
- package/dist/lib/types/artifacts.js +3 -0
- package/dist/lib/types/artifacts.js.map +1 -0
- package/dist/lib/types/buildinfo.d.ts +112 -0
- package/dist/lib/types/buildinfo.d.ts.map +1 -0
- package/dist/lib/types/buildinfo.js +3 -0
- package/dist/lib/types/buildinfo.js.map +1 -0
- package/dist/lib/types/conditions.d.ts +17 -0
- package/dist/lib/types/conditions.d.ts.map +1 -0
- package/dist/lib/types/conditions.js +21 -0
- package/dist/lib/types/conditions.js.map +1 -0
- package/dist/lib/types/contracts.d.ts +14 -0
- package/dist/lib/types/contracts.d.ts.map +1 -0
- package/dist/lib/types/contracts.js +3 -0
- package/dist/lib/types/contracts.js.map +1 -0
- package/dist/lib/types/definitions.d.ts +51 -0
- package/dist/lib/types/definitions.d.ts.map +1 -0
- package/dist/lib/types/definitions.js +3 -0
- package/dist/lib/types/definitions.js.map +1 -0
- package/dist/lib/types/index.d.ts +9 -0
- package/dist/lib/types/index.d.ts.map +1 -0
- package/dist/lib/types/index.js +25 -0
- package/dist/lib/types/index.js.map +1 -0
- package/dist/lib/types/network.d.ts +9 -0
- package/dist/lib/types/network.d.ts.map +1 -0
- package/dist/lib/types/network.js +3 -0
- package/dist/lib/types/network.js.map +1 -0
- package/dist/lib/types/project.d.ts +5 -0
- package/dist/lib/types/project.d.ts.map +1 -0
- package/dist/lib/types/project.js +3 -0
- package/dist/lib/types/project.js.map +1 -0
- package/dist/lib/types/task.d.ts +9 -0
- package/dist/lib/types/task.d.ts.map +1 -0
- package/dist/lib/types/task.js +3 -0
- package/dist/lib/types/task.js.map +1 -0
- package/dist/lib/types/values.d.ts +78 -0
- package/dist/lib/types/values.d.ts.map +1 -0
- package/dist/lib/types/values.js +3 -0
- package/dist/lib/types/values.js.map +1 -0
- package/dist/lib/utils/validation.d.ts +5 -0
- package/dist/lib/utils/validation.d.ts.map +1 -0
- package/dist/lib/utils/validation.js +77 -0
- package/dist/lib/utils/validation.js.map +1 -0
- package/dist/lib/validation/contract-references.d.ts +12 -0
- package/dist/lib/validation/contract-references.d.ts.map +1 -0
- package/dist/lib/validation/contract-references.js +112 -0
- package/dist/lib/validation/contract-references.js.map +1 -0
- package/dist/lib/validation/index.d.ts +1 -0
- package/dist/lib/validation/index.d.ts.map +1 -0
- package/dist/lib/validation/index.js +2 -0
- package/dist/lib/validation/index.js.map +1 -0
- package/dist/lib/verification/__tests__/etherscan.spec.d.ts +2 -0
- package/dist/lib/verification/__tests__/etherscan.spec.d.ts.map +1 -0
- package/dist/lib/verification/__tests__/etherscan.spec.js +565 -0
- package/dist/lib/verification/__tests__/etherscan.spec.js.map +1 -0
- package/dist/lib/verification/__tests__/sourcify.spec.d.ts +2 -0
- package/dist/lib/verification/__tests__/sourcify.spec.d.ts.map +1 -0
- package/dist/lib/verification/__tests__/sourcify.spec.js +212 -0
- package/dist/lib/verification/__tests__/sourcify.spec.js.map +1 -0
- package/dist/lib/verification/etherscan.d.ts +56 -0
- package/dist/lib/verification/etherscan.d.ts.map +1 -0
- package/dist/lib/verification/etherscan.js +340 -0
- package/dist/lib/verification/etherscan.js.map +1 -0
- package/dist/lib/verification/sourcify.d.ts +12 -0
- package/dist/lib/verification/sourcify.d.ts.map +1 -0
- package/dist/lib/verification/sourcify.js +227 -0
- package/dist/lib/verification/sourcify.js.map +1 -0
- package/eslint.config.js +48 -0
- package/examples/jobs/guards-v1.yaml +17 -0
- package/examples/jobs/sequence-seq-0001-patch.yaml +59 -0
- package/examples/jobs/sequence-v1.yaml +59 -0
- package/examples/templates/sequence-factory-v1.yaml +56 -0
- package/jest.config.js +25 -0
- package/package.json +68 -0
- package/src/cli.ts +17 -0
- package/src/commands/common.ts +61 -0
- package/src/commands/dry.ts +208 -0
- package/src/commands/etherscan.ts +360 -0
- package/src/commands/index.ts +5 -0
- package/src/commands/list.ts +249 -0
- package/src/commands/run.ts +136 -0
- package/src/commands/utils.ts +52 -0
- package/src/index.ts +67 -0
- package/src/lib/__tests__/deployer-events.spec.ts +338 -0
- package/src/lib/__tests__/deployer.spec.ts +1204 -0
- package/src/lib/__tests__/network-utils.spec.ts +181 -0
- package/src/lib/artifacts/__tests__/fixtures/contract1.json +19 -0
- package/src/lib/artifacts/__tests__/fixtures/contract2.json +19 -0
- package/src/lib/artifacts/__tests__/fixtures/duplicate-name.json +19 -0
- package/src/lib/artifacts/__tests__/fixtures/nested/nested-contract.json +18 -0
- package/src/lib/artifacts/__tests__/fixtures/not-an-artifact.json +8 -0
- package/src/lib/artifacts/__tests__/fixtures/readme.txt +2 -0
- package/src/lib/contracts/__tests__/repository.spec.ts +344 -0
- package/src/lib/contracts/repository.ts +313 -0
- package/src/lib/core/__tests__/engine.spec.ts +1514 -0
- package/src/lib/core/__tests__/graph.spec.ts +125 -0
- package/src/lib/core/__tests__/json-integration.spec.ts +360 -0
- package/src/lib/core/__tests__/loader.spec.ts +334 -0
- package/src/lib/core/__tests__/multi-platform-verification.spec.ts +406 -0
- package/src/lib/core/__tests__/resolver.spec.ts +1693 -0
- package/src/lib/core/__tests__/static-action.spec.ts +172 -0
- package/src/lib/core/context.ts +127 -0
- package/src/lib/core/engine.ts +1531 -0
- package/src/lib/core/graph.ts +252 -0
- package/src/lib/core/loader.ts +263 -0
- package/src/lib/core/resolver.ts +498 -0
- package/src/lib/deployer.ts +768 -0
- package/src/lib/events/__tests__/event-system.spec.ts +343 -0
- package/src/lib/events/cli-adapter.ts +325 -0
- package/src/lib/events/emitter.ts +62 -0
- package/src/lib/events/index.ts +3 -0
- package/src/lib/events/types.ts +469 -0
- package/src/lib/index.ts +14 -0
- package/src/lib/network-loader.ts +59 -0
- package/src/lib/network-utils.ts +64 -0
- package/src/lib/parsers/__tests__/buildinfo.spec.ts +122 -0
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-bytecode-buildinfo.json +62 -0
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-json.txt +2 -0
- package/src/lib/parsers/__tests__/fixtures/buildinfo/multi-contract-buildinfo.json +89 -0
- package/src/lib/parsers/__tests__/fixtures/buildinfo/no-contracts-buildinfo.json +17 -0
- package/src/lib/parsers/__tests__/fixtures/buildinfo/simple-buildinfo.json +63 -0
- package/src/lib/parsers/__tests__/fixtures/buildinfo/wrong-format.json +4 -0
- package/src/lib/parsers/__tests__/job.spec.ts +335 -0
- package/src/lib/parsers/__tests__/template.spec.ts +111 -0
- package/src/lib/parsers/artifact/__tests__/artifact.spec.ts +117 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/empty-bytecode.json +5 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/hardhat-artifact.json +67 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-bytecode.json +5 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-json.txt +11 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/minimal-artifact.json +5 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-abi.json +4 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-bytecode.json +11 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-contract-name.json +11 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/simple-artifact.json +40 -0
- package/src/lib/parsers/artifact/__tests__/fixtures/wrong-types.json +7 -0
- package/src/lib/parsers/artifact/foundry-1.2.ts +72 -0
- package/src/lib/parsers/artifact/index.ts +27 -0
- package/src/lib/parsers/artifact/types.ts +9 -0
- package/src/lib/parsers/buildinfo.ts +127 -0
- package/src/lib/parsers/constants.ts +56 -0
- package/src/lib/parsers/index.ts +5 -0
- package/src/lib/parsers/job.ts +101 -0
- package/src/lib/parsers/template.ts +131 -0
- package/src/lib/std/templates/assured-deployment.yaml +45 -0
- package/src/lib/std/templates/erc-2470.yaml +67 -0
- package/src/lib/std/templates/min-balance.yaml +32 -0
- package/src/lib/std/templates/nano-universal-deployer.yaml +59 -0
- package/src/lib/std/templates/raw-erc-2470.yaml +59 -0
- package/src/lib/std/templates/raw-nano-universal-deployer.yaml +51 -0
- package/src/lib/std/templates/raw-sequence-universal-deployer-2.yaml +48 -0
- package/src/lib/std/templates/sequence-universal-deployer-2.yaml +57 -0
- package/src/lib/types/__tests__/json-request-action.spec.ts +243 -0
- package/src/lib/types/__tests__/read-json-value.spec.ts +264 -0
- package/src/lib/types/actions.ts +127 -0
- package/src/lib/types/artifacts.ts +21 -0
- package/src/lib/types/buildinfo.ts +116 -0
- package/src/lib/types/conditions.ts +50 -0
- package/src/lib/types/contracts.ts +23 -0
- package/src/lib/types/definitions.ts +68 -0
- package/src/lib/types/index.ts +8 -0
- package/src/lib/types/network.ts +22 -0
- package/src/lib/types/project.ts +9 -0
- package/src/lib/types/task.ts +9 -0
- package/src/lib/types/values.ts +116 -0
- package/src/lib/utils/validation.ts +116 -0
- package/src/lib/validation/contract-references.ts +210 -0
- package/src/lib/validation/index.ts +1 -0
- package/src/lib/verification/__tests__/etherscan.spec.ts +710 -0
- package/src/lib/verification/__tests__/sourcify.spec.ts +288 -0
- package/src/lib/verification/etherscan.ts +546 -0
- package/src/lib/verification/sourcify.ts +248 -0
- package/test_validation/artifacts/TestContract.json +9 -0
- package/test_validation/jobs/test-missing.yaml +16 -0
- package/test_validation/networks.yaml +3 -0
- package/tsconfig.json +36 -0
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { Deployer, DeployerOptions } from '../deployer'
|
|
5
|
+
import { ProjectLoader } from '../core/loader'
|
|
6
|
+
import { DependencyGraph } from '../core/graph'
|
|
7
|
+
import { ExecutionEngine } from '../core/engine'
|
|
8
|
+
import { ExecutionContext } from '../core/context'
|
|
9
|
+
import { Network, Job, Template } from '../types'
|
|
10
|
+
|
|
11
|
+
// Mock all dependencies
|
|
12
|
+
jest.mock('fs/promises')
|
|
13
|
+
jest.mock('../core/loader')
|
|
14
|
+
jest.mock('../core/graph')
|
|
15
|
+
jest.mock('../core/engine')
|
|
16
|
+
jest.mock('../core/context')
|
|
17
|
+
|
|
18
|
+
const mockFs = fs as jest.Mocked<typeof fs>
|
|
19
|
+
const MockProjectLoader = ProjectLoader as jest.MockedClass<typeof ProjectLoader>
|
|
20
|
+
const MockDependencyGraph = DependencyGraph as jest.MockedClass<typeof DependencyGraph>
|
|
21
|
+
const MockExecutionEngine = ExecutionEngine as jest.MockedClass<typeof ExecutionEngine>
|
|
22
|
+
const MockExecutionContext = ExecutionContext as jest.MockedClass<typeof ExecutionContext>
|
|
23
|
+
|
|
24
|
+
describe('Deployer', () => {
|
|
25
|
+
let deployerOptions: DeployerOptions
|
|
26
|
+
let mockNetwork1: Network
|
|
27
|
+
let mockNetwork2: Network
|
|
28
|
+
let mockJob1: Job
|
|
29
|
+
let mockJob2: Job
|
|
30
|
+
let mockJob3: Job
|
|
31
|
+
let deprecatedJob: Job
|
|
32
|
+
let mockTemplate1: Template
|
|
33
|
+
let mockLoader: jest.Mocked<ProjectLoader>
|
|
34
|
+
let mockGraph: jest.Mocked<DependencyGraph>
|
|
35
|
+
let mockEngine: jest.Mocked<ExecutionEngine>
|
|
36
|
+
let mockContext: jest.Mocked<ExecutionContext>
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
// Clear all mocks
|
|
40
|
+
jest.clearAllMocks()
|
|
41
|
+
|
|
42
|
+
// Setup mock networks
|
|
43
|
+
mockNetwork1 = { name: 'mainnet', chainId: 1, rpcUrl: 'https://eth.rpc' }
|
|
44
|
+
mockNetwork2 = { name: 'polygon', chainId: 137, rpcUrl: 'https://polygon.rpc' }
|
|
45
|
+
|
|
46
|
+
// Setup mock jobs
|
|
47
|
+
mockJob1 = {
|
|
48
|
+
name: 'job1',
|
|
49
|
+
version: '1.0.0',
|
|
50
|
+
description: 'First job',
|
|
51
|
+
actions: [
|
|
52
|
+
{ name: 'action1', template: 'template1', arguments: {} }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mockJob2 = {
|
|
57
|
+
name: 'job2',
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
description: 'Second job',
|
|
60
|
+
depends_on: ['job1'],
|
|
61
|
+
actions: [
|
|
62
|
+
{ name: 'action2', template: 'template1', arguments: {} }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
mockJob3 = {
|
|
67
|
+
name: 'job3',
|
|
68
|
+
version: '1.0.0',
|
|
69
|
+
description: 'Third job with network filters',
|
|
70
|
+
only_networks: [1], // Only mainnet
|
|
71
|
+
actions: [
|
|
72
|
+
{ name: 'action3', template: 'template1', arguments: {} }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
deprecatedJob = {
|
|
77
|
+
name: 'legacy-job',
|
|
78
|
+
version: '0.1.0',
|
|
79
|
+
description: 'Deprecated job',
|
|
80
|
+
deprecated: true,
|
|
81
|
+
actions: [
|
|
82
|
+
{ name: 'legacy-action', template: 'template1', arguments: {} }
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Setup mock template
|
|
87
|
+
mockTemplate1 = {
|
|
88
|
+
name: 'template1',
|
|
89
|
+
actions: [
|
|
90
|
+
{ type: 'send-transaction', arguments: {} }
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Basic deployer options
|
|
95
|
+
deployerOptions = {
|
|
96
|
+
projectRoot: '/test/project',
|
|
97
|
+
privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
98
|
+
networks: [mockNetwork1, mockNetwork2],
|
|
99
|
+
flatOutput: true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Setup mocks
|
|
103
|
+
mockLoader = {
|
|
104
|
+
load: jest.fn(),
|
|
105
|
+
jobs: new Map([
|
|
106
|
+
['job1', mockJob1],
|
|
107
|
+
['job2', mockJob2],
|
|
108
|
+
['job3', mockJob3]
|
|
109
|
+
]),
|
|
110
|
+
templates: new Map([
|
|
111
|
+
['template1', mockTemplate1]
|
|
112
|
+
]),
|
|
113
|
+
contractRepository: {} as any
|
|
114
|
+
} as any
|
|
115
|
+
|
|
116
|
+
mockGraph = {
|
|
117
|
+
getExecutionOrder: jest.fn().mockReturnValue(['job1', 'job2', 'job3']),
|
|
118
|
+
getDependencies: jest.fn().mockReturnValue(new Set())
|
|
119
|
+
} as any
|
|
120
|
+
|
|
121
|
+
mockEngine = {
|
|
122
|
+
executeJob: jest.fn().mockResolvedValue(undefined)
|
|
123
|
+
} as any
|
|
124
|
+
|
|
125
|
+
mockContext = {
|
|
126
|
+
getOutputs: jest.fn().mockReturnValue(new Map<string, any>([
|
|
127
|
+
['action1.hash', '0xhash1'],
|
|
128
|
+
['action1.receipt', { status: 1 }]
|
|
129
|
+
])),
|
|
130
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
131
|
+
setOutput: jest.fn(),
|
|
132
|
+
getOutput: jest.fn()
|
|
133
|
+
} as any
|
|
134
|
+
|
|
135
|
+
MockProjectLoader.mockImplementation(() => mockLoader)
|
|
136
|
+
MockDependencyGraph.mockImplementation(() => mockGraph)
|
|
137
|
+
MockExecutionEngine.mockImplementation(() => mockEngine)
|
|
138
|
+
MockExecutionContext.mockImplementation(() => mockContext)
|
|
139
|
+
|
|
140
|
+
// Mock fs operations
|
|
141
|
+
mockFs.mkdir.mockResolvedValue(undefined)
|
|
142
|
+
mockFs.writeFile.mockResolvedValue(undefined)
|
|
143
|
+
|
|
144
|
+
// Mock console methods to prevent test output pollution
|
|
145
|
+
jest.spyOn(console, 'log').mockImplementation()
|
|
146
|
+
jest.spyOn(console, 'error').mockImplementation()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
afterEach(() => {
|
|
150
|
+
jest.clearAllMocks()
|
|
151
|
+
jest.restoreAllMocks()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('constructor', () => {
|
|
155
|
+
it('should create a deployer with valid options', () => {
|
|
156
|
+
const deployer = new Deployer(deployerOptions)
|
|
157
|
+
expect(deployer).toBeInstanceOf(Deployer)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should initialize ProjectLoader with correct project root', () => {
|
|
161
|
+
new Deployer(deployerOptions)
|
|
162
|
+
expect(MockProjectLoader).toHaveBeenCalledWith('/test/project', undefined)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('run', () => {
|
|
167
|
+
describe('happy paths', () => {
|
|
168
|
+
it('should successfully run a simple deployment', async () => {
|
|
169
|
+
const deployer = new Deployer(deployerOptions)
|
|
170
|
+
|
|
171
|
+
await deployer.run()
|
|
172
|
+
|
|
173
|
+
// Verify the flow
|
|
174
|
+
expect(mockLoader.load).toHaveBeenCalledTimes(1)
|
|
175
|
+
expect(MockDependencyGraph).toHaveBeenCalledWith(mockLoader.jobs, mockLoader.templates)
|
|
176
|
+
expect(mockGraph.getExecutionOrder).toHaveBeenCalledTimes(1)
|
|
177
|
+
expect(MockExecutionEngine).toHaveBeenCalledWith(mockLoader.templates, expect.any(Object), expect.any(Object), false)
|
|
178
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(5) // job1&job2 on 2 networks + job3 on 1 network
|
|
179
|
+
expect(MockExecutionContext).toHaveBeenCalledTimes(5)
|
|
180
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output', { recursive: true })
|
|
181
|
+
expect(mockFs.writeFile).toHaveBeenCalledTimes(3) // One file per job
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should run only specified jobs and their dependencies', async () => {
|
|
185
|
+
// Mock getDependencies for this specific test
|
|
186
|
+
mockGraph.getDependencies.mockImplementation((jobName: string) => {
|
|
187
|
+
if (jobName === 'job2') return new Set(['job1'])
|
|
188
|
+
return new Set()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const options: DeployerOptions = {
|
|
192
|
+
...deployerOptions,
|
|
193
|
+
runJobs: ['job2'] // This should also include job1 due to dependency
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const deployer = new Deployer(options)
|
|
197
|
+
await deployer.run()
|
|
198
|
+
|
|
199
|
+
// Should execute job1 (dependency) and job2, but not job3
|
|
200
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(4) // 2 jobs × 2 networks
|
|
201
|
+
|
|
202
|
+
// Verify it was called with the right jobs
|
|
203
|
+
const executedJobs = mockEngine.executeJob.mock.calls.map(call => call[0].name)
|
|
204
|
+
expect(executedJobs).toContain('job1')
|
|
205
|
+
expect(executedJobs).toContain('job2')
|
|
206
|
+
expect(executedJobs).not.toContain('job3')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should run only on specified networks', async () => {
|
|
210
|
+
const options: DeployerOptions = {
|
|
211
|
+
...deployerOptions,
|
|
212
|
+
runOnNetworks: [1] // Only mainnet
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const deployer = new Deployer(options)
|
|
216
|
+
await deployer.run()
|
|
217
|
+
|
|
218
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(3) // 3 jobs × 1 network
|
|
219
|
+
|
|
220
|
+
// Verify all calls were with mainnet
|
|
221
|
+
const usedNetworks = MockExecutionContext.mock.calls.map(call => call[0])
|
|
222
|
+
expect(usedNetworks).toHaveLength(3)
|
|
223
|
+
usedNetworks.forEach(network => {
|
|
224
|
+
expect(network.chainId).toBe(1)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should skip jobs based on network filters', async () => {
|
|
229
|
+
const deployer = new Deployer(deployerOptions)
|
|
230
|
+
await deployer.run()
|
|
231
|
+
|
|
232
|
+
// job3 has only_networks: [1], so should only run on mainnet
|
|
233
|
+
const job3Calls = mockEngine.executeJob.mock.calls.filter(call => call[0].name === 'job3')
|
|
234
|
+
expect(job3Calls).toHaveLength(1) // Only on mainnet
|
|
235
|
+
|
|
236
|
+
// Verify it was called with mainnet (check the MockExecutionContext calls)
|
|
237
|
+
const contextCallsForJob3 = MockExecutionContext.mock.calls.filter((_, index) => {
|
|
238
|
+
const engineCall = mockEngine.executeJob.mock.calls[index]
|
|
239
|
+
return engineCall && engineCall[0].name === 'job3'
|
|
240
|
+
})
|
|
241
|
+
expect(contextCallsForJob3[0][0].chainId).toBe(1)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should handle jobs with skip_networks filter', async () => {
|
|
245
|
+
const jobWithSkipNetworks: Job = {
|
|
246
|
+
...mockJob1,
|
|
247
|
+
name: 'job-skip-polygon',
|
|
248
|
+
skip_networks: [137] // Skip polygon
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
mockLoader.jobs.set('job-skip-polygon', jobWithSkipNetworks)
|
|
252
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-skip-polygon'])
|
|
253
|
+
|
|
254
|
+
const deployer = new Deployer(deployerOptions)
|
|
255
|
+
await deployer.run()
|
|
256
|
+
|
|
257
|
+
// Should only run on mainnet (chainId 1), not polygon (chainId 137)
|
|
258
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(1)
|
|
259
|
+
const usedNetwork = MockExecutionContext.mock.calls[0][0]
|
|
260
|
+
expect(usedNetwork.chainId).toBe(1)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should create correct output files in flat mode', async () => {
|
|
264
|
+
const deployer = new Deployer({ ...deployerOptions, flatOutput: true })
|
|
265
|
+
await deployer.run()
|
|
266
|
+
|
|
267
|
+
// Verify output directory creation
|
|
268
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output', { recursive: true })
|
|
269
|
+
|
|
270
|
+
// Verify output files (flat)
|
|
271
|
+
expect(mockFs.writeFile).toHaveBeenCalledTimes(3)
|
|
272
|
+
|
|
273
|
+
// Check job1 output file (flat path)
|
|
274
|
+
const job1OutputCall = mockFs.writeFile.mock.calls.find(call =>
|
|
275
|
+
call[0] === '/test/project/output/job1.json'
|
|
276
|
+
)
|
|
277
|
+
expect(job1OutputCall).toBeDefined()
|
|
278
|
+
|
|
279
|
+
const job1Content = JSON.parse(job1OutputCall![1] as string)
|
|
280
|
+
expect(job1Content).toMatchObject({
|
|
281
|
+
jobName: 'job1',
|
|
282
|
+
jobVersion: '1.0.0',
|
|
283
|
+
lastRun: expect.any(String),
|
|
284
|
+
networks: [
|
|
285
|
+
{
|
|
286
|
+
status: 'success',
|
|
287
|
+
chainIds: expect.arrayContaining(['1', '137']),
|
|
288
|
+
outputs: expect.any(Object)
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should mirror jobs directory structure by default', async () => {
|
|
295
|
+
// Attach source paths to jobs to simulate their locations
|
|
296
|
+
const job1 = mockLoader.jobs.get('job1') as any
|
|
297
|
+
const job2 = mockLoader.jobs.get('job2') as any
|
|
298
|
+
const job3 = mockLoader.jobs.get('job3') as any
|
|
299
|
+
job1._path = '/test/project/jobs/core/job1.yaml'
|
|
300
|
+
job2._path = '/test/project/jobs/patches/job2.yml'
|
|
301
|
+
job3._path = '/test/project/jobs/job3.yaml'
|
|
302
|
+
|
|
303
|
+
const deployer = new Deployer({ ...deployerOptions, flatOutput: undefined })
|
|
304
|
+
await deployer.run()
|
|
305
|
+
|
|
306
|
+
// Should create nested directories
|
|
307
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output/core', { recursive: true })
|
|
308
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/output/patches', { recursive: true })
|
|
309
|
+
|
|
310
|
+
// job1.json under core
|
|
311
|
+
const job1OutputCall = mockFs.writeFile.mock.calls.find(call =>
|
|
312
|
+
call[0] === '/test/project/output/core/job1.json'
|
|
313
|
+
)
|
|
314
|
+
expect(job1OutputCall).toBeDefined()
|
|
315
|
+
|
|
316
|
+
// job2.json under patches
|
|
317
|
+
const job2OutputCall = mockFs.writeFile.mock.calls.find(call =>
|
|
318
|
+
call[0] === '/test/project/output/patches/job2.json'
|
|
319
|
+
)
|
|
320
|
+
expect(job2OutputCall).toBeDefined()
|
|
321
|
+
|
|
322
|
+
// job3 at root (no subdir)
|
|
323
|
+
const job3OutputCall = mockFs.writeFile.mock.calls.find(call =>
|
|
324
|
+
call[0] === '/test/project/output/job3.json'
|
|
325
|
+
)
|
|
326
|
+
expect(job3OutputCall).toBeDefined()
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('should handle empty project gracefully', async () => {
|
|
330
|
+
mockLoader.jobs.clear()
|
|
331
|
+
mockLoader.templates.clear()
|
|
332
|
+
mockGraph.getExecutionOrder.mockReturnValue([])
|
|
333
|
+
|
|
334
|
+
const deployer = new Deployer(deployerOptions)
|
|
335
|
+
await deployer.run()
|
|
336
|
+
|
|
337
|
+
expect(mockEngine.executeJob).not.toHaveBeenCalled()
|
|
338
|
+
expect(mockFs.writeFile).not.toHaveBeenCalled()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('should filter outputs based on action output flags', async () => {
|
|
342
|
+
// Create a job with mixed output flags
|
|
343
|
+
const jobWithOutputFlags: Job = {
|
|
344
|
+
name: 'job-with-output-flags',
|
|
345
|
+
version: '1.0.0',
|
|
346
|
+
description: 'Job with output filtering',
|
|
347
|
+
actions: [
|
|
348
|
+
{ name: 'deploy-action', template: 'template1', arguments: {}, output: true },
|
|
349
|
+
{ name: 'verify-action', template: 'template1', arguments: {}, output: false },
|
|
350
|
+
{ name: 'other-action', template: 'template1', arguments: {} } // no output flag
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
mockLoader.jobs.clear()
|
|
355
|
+
mockLoader.jobs.set('job-with-output-flags', jobWithOutputFlags)
|
|
356
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-with-output-flags'])
|
|
357
|
+
|
|
358
|
+
// Mock context to return outputs from all actions
|
|
359
|
+
mockContext.getOutputs.mockReturnValue(new Map<string, any>([
|
|
360
|
+
['deploy-action.address', '0xdeployaddress'],
|
|
361
|
+
['deploy-action.hash', '0xdeployhash'],
|
|
362
|
+
['verify-action.guid', 'verification-guid'],
|
|
363
|
+
['other-action.result', 'some-result']
|
|
364
|
+
]))
|
|
365
|
+
|
|
366
|
+
const deployer = new Deployer(deployerOptions)
|
|
367
|
+
await deployer.run()
|
|
368
|
+
|
|
369
|
+
// Verify output file was written
|
|
370
|
+
expect(mockFs.writeFile).toHaveBeenCalledTimes(1)
|
|
371
|
+
|
|
372
|
+
const outputCall = mockFs.writeFile.mock.calls[0]
|
|
373
|
+
expect(outputCall[0]).toBe('/test/project/output/job-with-output-flags.json')
|
|
374
|
+
|
|
375
|
+
const outputContent = JSON.parse(outputCall[1] as string)
|
|
376
|
+
expect(outputContent.networks).toHaveLength(1)
|
|
377
|
+
expect(outputContent.networks[0].status).toBe('success')
|
|
378
|
+
|
|
379
|
+
// Should only include outputs from deploy-action (output: true)
|
|
380
|
+
// Should NOT include verify-action (output: false) or other-action (no flag)
|
|
381
|
+
expect(outputContent.networks[0].outputs).toEqual({
|
|
382
|
+
'deploy-action.address': '0xdeployaddress',
|
|
383
|
+
'deploy-action.hash': '0xdeployhash'
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should include all outputs when no actions have output: true (backward compatibility)', async () => {
|
|
388
|
+
// Create a job where no actions explicitly set output: true
|
|
389
|
+
const jobWithoutOutputFlags: Job = {
|
|
390
|
+
name: 'job-without-output-flags',
|
|
391
|
+
version: '1.0.0',
|
|
392
|
+
description: 'Job without output flags',
|
|
393
|
+
actions: [
|
|
394
|
+
{ name: 'action1', template: 'template1', arguments: {} },
|
|
395
|
+
{ name: 'action2', template: 'template1', arguments: {}, output: false }
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
mockLoader.jobs.clear()
|
|
400
|
+
mockLoader.jobs.set('job-without-output-flags', jobWithoutOutputFlags)
|
|
401
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-without-output-flags'])
|
|
402
|
+
|
|
403
|
+
// Mock context to return outputs from all actions
|
|
404
|
+
mockContext.getOutputs.mockReturnValue(new Map<string, any>([
|
|
405
|
+
['action1.result', 'result1'],
|
|
406
|
+
['action2.result', 'result2']
|
|
407
|
+
]))
|
|
408
|
+
|
|
409
|
+
const deployer = new Deployer(deployerOptions)
|
|
410
|
+
await deployer.run()
|
|
411
|
+
|
|
412
|
+
// Verify output file was written
|
|
413
|
+
expect(mockFs.writeFile).toHaveBeenCalledTimes(1)
|
|
414
|
+
|
|
415
|
+
const outputCall = mockFs.writeFile.mock.calls[0]
|
|
416
|
+
const outputContent = JSON.parse(outputCall[1] as string)
|
|
417
|
+
|
|
418
|
+
// Should include all outputs (backward compatibility)
|
|
419
|
+
expect(outputContent.networks[0].outputs).toEqual({
|
|
420
|
+
'action1.result': 'result1',
|
|
421
|
+
'action2.result': 'result2'
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('should filter outputs correctly when multiple actions have output: true', async () => {
|
|
426
|
+
// Create a job with multiple actions marked for output
|
|
427
|
+
const jobWithMultipleOutputs: Job = {
|
|
428
|
+
name: 'job-multiple-outputs',
|
|
429
|
+
version: '1.0.0',
|
|
430
|
+
description: 'Job with multiple output actions',
|
|
431
|
+
actions: [
|
|
432
|
+
{ name: 'deploy1', template: 'template1', arguments: {}, output: true },
|
|
433
|
+
{ name: 'deploy2', template: 'template1', arguments: {}, output: true },
|
|
434
|
+
{ name: 'verify1', template: 'template1', arguments: {}, output: false },
|
|
435
|
+
{ name: 'verify2', template: 'template1', arguments: {}, output: false }
|
|
436
|
+
]
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
mockLoader.jobs.clear()
|
|
440
|
+
mockLoader.jobs.set('job-multiple-outputs', jobWithMultipleOutputs)
|
|
441
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-multiple-outputs'])
|
|
442
|
+
|
|
443
|
+
// Mock context to return outputs from all actions
|
|
444
|
+
mockContext.getOutputs.mockReturnValue(new Map<string, any>([
|
|
445
|
+
['deploy1.address', '0xdeploy1'],
|
|
446
|
+
['deploy2.address', '0xdeploy2'],
|
|
447
|
+
['verify1.guid', 'verify1-guid'],
|
|
448
|
+
['verify2.guid', 'verify2-guid']
|
|
449
|
+
]))
|
|
450
|
+
|
|
451
|
+
const deployer = new Deployer(deployerOptions)
|
|
452
|
+
await deployer.run()
|
|
453
|
+
|
|
454
|
+
// Verify output file was written
|
|
455
|
+
const outputCall = mockFs.writeFile.mock.calls[0]
|
|
456
|
+
const outputContent = JSON.parse(outputCall[1] as string)
|
|
457
|
+
|
|
458
|
+
// Should include outputs from both deploy actions, but not verify actions
|
|
459
|
+
expect(outputContent.networks[0].outputs).toEqual({
|
|
460
|
+
'deploy1.address': '0xdeploy1',
|
|
461
|
+
'deploy2.address': '0xdeploy2'
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
describe('error handling', () => {
|
|
467
|
+
it('should throw when project loading fails', async () => {
|
|
468
|
+
mockLoader.load.mockRejectedValue(new Error('Failed to load project'))
|
|
469
|
+
|
|
470
|
+
const deployer = new Deployer(deployerOptions)
|
|
471
|
+
|
|
472
|
+
await expect(deployer.run()).rejects.toThrow('Failed to load project')
|
|
473
|
+
// Note: Error handling is now done via events, not console.error directly
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('should throw when dependency graph creation fails', async () => {
|
|
477
|
+
MockDependencyGraph.mockImplementation(() => {
|
|
478
|
+
throw new Error('Circular dependency detected')
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const deployer = new Deployer(deployerOptions)
|
|
482
|
+
|
|
483
|
+
await expect(deployer.run()).rejects.toThrow('Circular dependency detected')
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it('should capture job execution failures and then throw', async () => {
|
|
487
|
+
mockEngine.executeJob.mockRejectedValue(new Error('Transaction failed'))
|
|
488
|
+
|
|
489
|
+
const deployer = new Deployer(deployerOptions)
|
|
490
|
+
|
|
491
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
492
|
+
|
|
493
|
+
// Should still write output files with error entries before throwing
|
|
494
|
+
expect(mockFs.writeFile).toHaveBeenCalled()
|
|
495
|
+
|
|
496
|
+
// Check that error entries are recorded
|
|
497
|
+
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
498
|
+
const outputFile = writeFileCalls[0]
|
|
499
|
+
const outputContent = JSON.parse(outputFile[1] as string)
|
|
500
|
+
|
|
501
|
+
// Should have error entries for failed executions
|
|
502
|
+
const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
|
|
503
|
+
expect(errorEntries.length).toBeGreaterThan(0)
|
|
504
|
+
expect(errorEntries[0].error).toBe('Transaction failed')
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('should throw when output directory creation fails', async () => {
|
|
508
|
+
mockFs.mkdir.mockRejectedValue(new Error('Permission denied'))
|
|
509
|
+
|
|
510
|
+
const deployer = new Deployer(deployerOptions)
|
|
511
|
+
|
|
512
|
+
await expect(deployer.run()).rejects.toThrow('Permission denied')
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('should throw when output file writing fails', async () => {
|
|
516
|
+
mockFs.writeFile.mockRejectedValue(new Error('Disk full'))
|
|
517
|
+
|
|
518
|
+
const deployer = new Deployer(deployerOptions)
|
|
519
|
+
|
|
520
|
+
await expect(deployer.run()).rejects.toThrow('Disk full')
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('should handle execution context creation failure and then throw', async () => {
|
|
524
|
+
MockExecutionContext.mockImplementation(() => {
|
|
525
|
+
throw new Error('Invalid private key')
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
const deployer = new Deployer(deployerOptions)
|
|
529
|
+
|
|
530
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
531
|
+
|
|
532
|
+
// Should record context creation failures as error entries before throwing
|
|
533
|
+
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
534
|
+
const outputFile = writeFileCalls[0]
|
|
535
|
+
const outputContent = JSON.parse(outputFile[1] as string)
|
|
536
|
+
|
|
537
|
+
const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
|
|
538
|
+
expect(errorEntries.length).toBeGreaterThan(0)
|
|
539
|
+
expect(errorEntries[0].error).toBe('Invalid private key')
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
describe('edge cases and weird scenarios', () => {
|
|
544
|
+
it('should handle job with only_networks that includes non-existent network', async () => {
|
|
545
|
+
const weirdJob: Job = {
|
|
546
|
+
...mockJob1,
|
|
547
|
+
name: 'weird-job',
|
|
548
|
+
only_networks: [999] // Non-existent network
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
mockLoader.jobs.clear()
|
|
552
|
+
mockLoader.jobs.set('weird-job', weirdJob)
|
|
553
|
+
mockGraph.getExecutionOrder.mockReturnValue(['weird-job'])
|
|
554
|
+
|
|
555
|
+
const deployer = new Deployer(deployerOptions)
|
|
556
|
+
await deployer.run()
|
|
557
|
+
|
|
558
|
+
// Should not execute on any network
|
|
559
|
+
expect(mockEngine.executeJob).not.toHaveBeenCalled()
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('should handle job with skip_networks that includes all networks', async () => {
|
|
563
|
+
const weirdJob: Job = {
|
|
564
|
+
...mockJob1,
|
|
565
|
+
name: 'weird-job',
|
|
566
|
+
skip_networks: [1, 137] // Skip all available networks
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
mockLoader.jobs.clear()
|
|
570
|
+
mockLoader.jobs.set('weird-job', weirdJob)
|
|
571
|
+
mockGraph.getExecutionOrder.mockReturnValue(['weird-job'])
|
|
572
|
+
|
|
573
|
+
const deployer = new Deployer(deployerOptions)
|
|
574
|
+
await deployer.run()
|
|
575
|
+
|
|
576
|
+
// Should not execute on any network
|
|
577
|
+
expect(mockEngine.executeJob).not.toHaveBeenCalled()
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it('should handle runOnNetworks with non-existent chain IDs', async () => {
|
|
581
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
582
|
+
|
|
583
|
+
const options: DeployerOptions = {
|
|
584
|
+
...deployerOptions,
|
|
585
|
+
runOnNetworks: [1, 999, 888] // 999 and 888 don't exist
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const deployer = new Deployer(options)
|
|
589
|
+
await deployer.run()
|
|
590
|
+
|
|
591
|
+
// Note: Warnings are now emitted as events, not console.warn directly
|
|
592
|
+
// The CLI adapter converts events to console output
|
|
593
|
+
|
|
594
|
+
// Should only execute on the existing network (chainId 1)
|
|
595
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(3) // 3 jobs × 1 network
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it('should handle runJobs with non-existent job names', async () => {
|
|
599
|
+
const options: DeployerOptions = {
|
|
600
|
+
...deployerOptions,
|
|
601
|
+
runJobs: ['non-existent-job']
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const deployer = new Deployer(options)
|
|
605
|
+
|
|
606
|
+
await expect(deployer.run()).rejects.toThrow(
|
|
607
|
+
'Specified job "non-existent-job" not found in project.'
|
|
608
|
+
)
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
it('should handle execution context without getOutputs method and then throw', async () => {
|
|
612
|
+
const brokenContext = {
|
|
613
|
+
// Missing getOutputs method
|
|
614
|
+
} as any
|
|
615
|
+
|
|
616
|
+
MockExecutionContext.mockImplementation(() => brokenContext)
|
|
617
|
+
|
|
618
|
+
const deployer = new Deployer(deployerOptions)
|
|
619
|
+
|
|
620
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
621
|
+
|
|
622
|
+
// Should record the missing method error before throwing
|
|
623
|
+
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
624
|
+
const outputFile = writeFileCalls[0]
|
|
625
|
+
const outputContent = JSON.parse(outputFile[1] as string)
|
|
626
|
+
|
|
627
|
+
const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
|
|
628
|
+
expect(errorEntries.length).toBeGreaterThan(0)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
it('should handle empty networks array', async () => {
|
|
632
|
+
const options: DeployerOptions = {
|
|
633
|
+
...deployerOptions,
|
|
634
|
+
networks: []
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const deployer = new Deployer(options)
|
|
638
|
+
await deployer.run()
|
|
639
|
+
|
|
640
|
+
// Should not execute anything
|
|
641
|
+
expect(mockEngine.executeJob).not.toHaveBeenCalled()
|
|
642
|
+
expect(mockFs.writeFile).not.toHaveBeenCalled()
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('should handle empty runJobs array', async () => {
|
|
646
|
+
const options: DeployerOptions = {
|
|
647
|
+
...deployerOptions,
|
|
648
|
+
runJobs: []
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const deployer = new Deployer(options)
|
|
652
|
+
await deployer.run()
|
|
653
|
+
|
|
654
|
+
// Should run all jobs
|
|
655
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(5) // job1&job2 on 2 networks + job3 on 1 network
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
it('should handle empty runOnNetworks array', async () => {
|
|
659
|
+
const options: DeployerOptions = {
|
|
660
|
+
...deployerOptions,
|
|
661
|
+
runOnNetworks: []
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const deployer = new Deployer(options)
|
|
665
|
+
await deployer.run()
|
|
666
|
+
|
|
667
|
+
// Should run on all networks
|
|
668
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(5) // job1&job2 on 2 networks + job3 on 1 network
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('should handle job with both only_networks and skip_networks', async () => {
|
|
672
|
+
const conflictedJob: Job = {
|
|
673
|
+
...mockJob1,
|
|
674
|
+
name: 'conflicted-job',
|
|
675
|
+
only_networks: [1, 137],
|
|
676
|
+
skip_networks: [137]
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
mockLoader.jobs.clear()
|
|
680
|
+
mockLoader.jobs.set('conflicted-job', conflictedJob)
|
|
681
|
+
mockGraph.getExecutionOrder.mockReturnValue(['conflicted-job'])
|
|
682
|
+
|
|
683
|
+
const deployer = new Deployer(deployerOptions)
|
|
684
|
+
await deployer.run()
|
|
685
|
+
|
|
686
|
+
// only_networks takes precedence, so should run on [1, 137]
|
|
687
|
+
// skip_networks is ignored when only_networks is present
|
|
688
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
|
|
689
|
+
const usedNetworks = MockExecutionContext.mock.calls.map(call => call[0].chainId)
|
|
690
|
+
expect(usedNetworks).toEqual(expect.arrayContaining([1, 137]))
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
it('should write output files even when all executions fail and then throw', async () => {
|
|
694
|
+
// Make all executions fail
|
|
695
|
+
mockEngine.executeJob.mockImplementation(() => {
|
|
696
|
+
throw new Error('Execution failed')
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
const deployer = new Deployer(deployerOptions)
|
|
700
|
+
|
|
701
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
702
|
+
|
|
703
|
+
// Should write output files with error entries before throwing
|
|
704
|
+
expect(mockFs.writeFile).toHaveBeenCalled()
|
|
705
|
+
|
|
706
|
+
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
707
|
+
const outputFile = writeFileCalls[0]
|
|
708
|
+
const outputContent = JSON.parse(outputFile[1] as string)
|
|
709
|
+
|
|
710
|
+
// All entries should be error entries
|
|
711
|
+
const errorEntries = outputContent.networks.filter((entry: any) => entry.status === 'error')
|
|
712
|
+
expect(errorEntries.length).toBeGreaterThan(0)
|
|
713
|
+
|
|
714
|
+
// No success entries
|
|
715
|
+
const successEntries = outputContent.networks.filter((entry: any) => entry.status === 'success')
|
|
716
|
+
expect(successEntries.length).toBe(0)
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
it('should handle very long execution order', async () => {
|
|
720
|
+
// Create 100 jobs to test performance/memory
|
|
721
|
+
const manyJobs = Array.from({ length: 100 }, (_, i) => `job${i}`)
|
|
722
|
+
mockGraph.getExecutionOrder.mockReturnValue(manyJobs)
|
|
723
|
+
|
|
724
|
+
// Mock loader to have all these jobs
|
|
725
|
+
for (let i = 0; i < 100; i++) {
|
|
726
|
+
mockLoader.jobs.set(`job${i}`, {
|
|
727
|
+
...mockJob1,
|
|
728
|
+
name: `job${i}`
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const deployer = new Deployer(deployerOptions)
|
|
733
|
+
await deployer.run()
|
|
734
|
+
|
|
735
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(200) // 100 jobs × 2 networks
|
|
736
|
+
expect(mockFs.writeFile).toHaveBeenCalledTimes(100) // One file per job
|
|
737
|
+
})
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
describe('private method testing', () => {
|
|
741
|
+
let deployer: Deployer
|
|
742
|
+
|
|
743
|
+
beforeEach(() => {
|
|
744
|
+
deployer = new Deployer(deployerOptions)
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
describe('getJobExecutionPlan', () => {
|
|
748
|
+
it('should return full order when no runJobs specified', () => {
|
|
749
|
+
const fullOrder = ['job1', 'job2', 'job3']
|
|
750
|
+
const plan = (deployer as any).getJobExecutionPlan(fullOrder)
|
|
751
|
+
expect(plan).toEqual(fullOrder)
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
it('should filter and include dependencies', async () => {
|
|
755
|
+
const options: DeployerOptions = {
|
|
756
|
+
...deployerOptions,
|
|
757
|
+
runJobs: ['job2']
|
|
758
|
+
}
|
|
759
|
+
const deployer = new Deployer(options)
|
|
760
|
+
|
|
761
|
+
// Initialize the deployer's graph by calling load
|
|
762
|
+
await mockLoader.load()
|
|
763
|
+
;(deployer as any).graph = mockGraph
|
|
764
|
+
|
|
765
|
+
// Mock getDependencies to return job1 as dependency of job2
|
|
766
|
+
mockGraph.getDependencies.mockReturnValueOnce(new Set(['job1']))
|
|
767
|
+
|
|
768
|
+
const fullOrder = ['job1', 'job2', 'job3']
|
|
769
|
+
const plan = (deployer as any).getJobExecutionPlan(fullOrder)
|
|
770
|
+
expect(plan).toEqual(['job1', 'job2'])
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
it('should include deprecated dependencies when no runJobs specified', () => {
|
|
774
|
+
// Add deprecated job and make job2 depend on it transitively
|
|
775
|
+
;(mockLoader.jobs as Map<string, Job>).set('legacy-job', deprecatedJob)
|
|
776
|
+
|
|
777
|
+
// full order includes all
|
|
778
|
+
const fullOrder = ['legacy-job', 'job1', 'job2', 'job3']
|
|
779
|
+
|
|
780
|
+
// Mock dependency graph: job2 depends on job1 and legacy-job
|
|
781
|
+
mockGraph.getDependencies.mockImplementation((jobName: string) => {
|
|
782
|
+
if (jobName === 'job2') return new Set(['job1', 'legacy-job'])
|
|
783
|
+
return new Set()
|
|
784
|
+
})
|
|
785
|
+
;(deployer as any).graph = mockGraph
|
|
786
|
+
const plan = (deployer as any).getJobExecutionPlan(fullOrder)
|
|
787
|
+
// Expect legacy-job to be included because it is a dependency of non-deprecated job2
|
|
788
|
+
expect(plan).toEqual(['legacy-job', 'job1', 'job2', 'job3'])
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
it('should keep deprecated dependencies when specific jobs are requested', async () => {
|
|
792
|
+
// Add deprecated job and dependency relation
|
|
793
|
+
;(mockLoader.jobs as Map<string, Job>).set('legacy-job', deprecatedJob)
|
|
794
|
+
const options: DeployerOptions = {
|
|
795
|
+
...deployerOptions,
|
|
796
|
+
runJobs: ['job2']
|
|
797
|
+
}
|
|
798
|
+
const depDeployer = new Deployer(options)
|
|
799
|
+
;(depDeployer as any).graph = mockGraph
|
|
800
|
+
|
|
801
|
+
mockGraph.getDependencies.mockImplementation((jobName: string) => {
|
|
802
|
+
if (jobName === 'job2') return new Set(['job1', 'legacy-job'])
|
|
803
|
+
return new Set()
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
const fullOrder = ['legacy-job', 'job1', 'job2', 'job3']
|
|
807
|
+
const plan = (depDeployer as any).getJobExecutionPlan(fullOrder)
|
|
808
|
+
expect(plan).toEqual(['legacy-job', 'job1', 'job2'])
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('should expand wildcard patterns in runJobs and preserve execution order', async () => {
|
|
812
|
+
;(mockLoader.jobs as Map<string, Job>).set('job10', { ...mockJob1, name: 'job10' })
|
|
813
|
+
;(mockLoader.jobs as Map<string, Job>).set('another', { ...mockJob1, name: 'another' })
|
|
814
|
+
|
|
815
|
+
const fullOrder = ['another', 'job1', 'job2', 'job3', 'job10']
|
|
816
|
+
mockGraph.getExecutionOrder.mockReturnValue(fullOrder)
|
|
817
|
+
|
|
818
|
+
const options: DeployerOptions = {
|
|
819
|
+
...deployerOptions,
|
|
820
|
+
runJobs: ['job*']
|
|
821
|
+
}
|
|
822
|
+
const dep = new Deployer(options)
|
|
823
|
+
;(dep as any).loader = mockLoader
|
|
824
|
+
;(dep as any).graph = mockGraph
|
|
825
|
+
|
|
826
|
+
const plan = (dep as any).getJobExecutionPlan(fullOrder)
|
|
827
|
+
expect(plan).toEqual(['job1', 'job2', 'job3', 'job10'])
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
it('should support mixed exact names and patterns', async () => {
|
|
831
|
+
const fullOrder = ['job1', 'job2', 'job3']
|
|
832
|
+
mockGraph.getExecutionOrder.mockReturnValue(fullOrder)
|
|
833
|
+
|
|
834
|
+
const options: DeployerOptions = {
|
|
835
|
+
...deployerOptions,
|
|
836
|
+
runJobs: ['job1', 'job?']
|
|
837
|
+
}
|
|
838
|
+
const dep = new Deployer(options)
|
|
839
|
+
;(dep as any).loader = mockLoader
|
|
840
|
+
;(dep as any).graph = mockGraph
|
|
841
|
+
|
|
842
|
+
const plan = (dep as any).getJobExecutionPlan(fullOrder)
|
|
843
|
+
expect(plan).toEqual(['job1', 'job2', 'job3'])
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
it('should throw when a pattern matches no jobs', async () => {
|
|
847
|
+
const fullOrder = ['job1', 'job2', 'job3']
|
|
848
|
+
mockGraph.getExecutionOrder.mockReturnValue(fullOrder)
|
|
849
|
+
|
|
850
|
+
const options: DeployerOptions = {
|
|
851
|
+
...deployerOptions,
|
|
852
|
+
runJobs: ['does-not-exist*']
|
|
853
|
+
}
|
|
854
|
+
const dep = new Deployer(options)
|
|
855
|
+
;(dep as any).loader = mockLoader
|
|
856
|
+
;(dep as any).graph = mockGraph
|
|
857
|
+
|
|
858
|
+
expect(() => (dep as any).getJobExecutionPlan(fullOrder)).toThrow(
|
|
859
|
+
'Job pattern "does-not-exist*" did not match any jobs in project.'
|
|
860
|
+
)
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
it('should match names containing slashes with patterns', async () => {
|
|
864
|
+
const jA: Job = { ...mockJob1, name: 'sequence_v3/beta_4' }
|
|
865
|
+
const jB: Job = { ...mockJob1, name: 'sequence_v3/rc_1' }
|
|
866
|
+
;(mockLoader.jobs as Map<string, Job>).set(jA.name, jA)
|
|
867
|
+
;(mockLoader.jobs as Map<string, Job>).set(jB.name, jB)
|
|
868
|
+
|
|
869
|
+
const fullOrder = ['job1', jA.name, jB.name, 'job2']
|
|
870
|
+
mockGraph.getExecutionOrder.mockReturnValue(fullOrder)
|
|
871
|
+
|
|
872
|
+
const options: DeployerOptions = {
|
|
873
|
+
...deployerOptions,
|
|
874
|
+
runJobs: ['sequence_v3/*']
|
|
875
|
+
}
|
|
876
|
+
const dep = new Deployer(options)
|
|
877
|
+
;(dep as any).loader = mockLoader
|
|
878
|
+
;(dep as any).graph = mockGraph
|
|
879
|
+
|
|
880
|
+
const plan = (dep as any).getJobExecutionPlan(fullOrder)
|
|
881
|
+
expect(plan).toEqual(['sequence_v3/beta_4', 'sequence_v3/rc_1'])
|
|
882
|
+
})
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
describe('getTargetNetworks', () => {
|
|
886
|
+
it('should return all networks when no runOnNetworks specified', () => {
|
|
887
|
+
const networks = (deployer as any).getTargetNetworks()
|
|
888
|
+
expect(networks).toEqual([mockNetwork1, mockNetwork2])
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
it('should filter networks by chain ID', () => {
|
|
892
|
+
const options: DeployerOptions = {
|
|
893
|
+
...deployerOptions,
|
|
894
|
+
runOnNetworks: [1]
|
|
895
|
+
}
|
|
896
|
+
const deployer = new Deployer(options)
|
|
897
|
+
|
|
898
|
+
const networks = (deployer as any).getTargetNetworks()
|
|
899
|
+
expect(networks).toEqual([mockNetwork1])
|
|
900
|
+
})
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
describe('shouldSkipJobOnNetwork', () => {
|
|
904
|
+
it('should return false for job with no network filters', () => {
|
|
905
|
+
const result = (deployer as any).shouldSkipJobOnNetwork(mockJob1, mockNetwork1)
|
|
906
|
+
expect(result).toBe(false)
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
it('should return true when network not in only_networks', () => {
|
|
910
|
+
const result = (deployer as any).shouldSkipJobOnNetwork(mockJob3, mockNetwork2)
|
|
911
|
+
expect(result).toBe(true)
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
it('should return false when network is in only_networks', () => {
|
|
915
|
+
const result = (deployer as any).shouldSkipJobOnNetwork(mockJob3, mockNetwork1)
|
|
916
|
+
expect(result).toBe(false)
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
it('should return true when network is in skip_networks', () => {
|
|
920
|
+
const jobWithSkip = {
|
|
921
|
+
...mockJob1,
|
|
922
|
+
skip_networks: [1]
|
|
923
|
+
}
|
|
924
|
+
const result = (deployer as any).shouldSkipJobOnNetwork(jobWithSkip, mockNetwork1)
|
|
925
|
+
expect(result).toBe(true)
|
|
926
|
+
})
|
|
927
|
+
})
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
describe('integration-like scenarios', () => {
|
|
931
|
+
it('should handle complex dependency chain with network filtering', async () => {
|
|
932
|
+
// Create a complex scenario:
|
|
933
|
+
// job1 -> job2 -> job3
|
|
934
|
+
// job3 only runs on mainnet
|
|
935
|
+
// job4 skips polygon
|
|
936
|
+
const job4: Job = {
|
|
937
|
+
name: 'job4',
|
|
938
|
+
version: '1.0.0',
|
|
939
|
+
depends_on: ['job3'],
|
|
940
|
+
skip_networks: [137],
|
|
941
|
+
actions: [{ name: 'action4', template: 'template1', arguments: {} }]
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
mockLoader.jobs.set('job4', job4)
|
|
945
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job1', 'job2', 'job3', 'job4'])
|
|
946
|
+
|
|
947
|
+
// Mock dependencies
|
|
948
|
+
mockGraph.getDependencies
|
|
949
|
+
.mockReturnValueOnce(new Set()) // job1 has no deps
|
|
950
|
+
.mockReturnValueOnce(new Set(['job1'])) // job2 depends on job1
|
|
951
|
+
.mockReturnValueOnce(new Set(['job1', 'job2'])) // job3 depends on job1, job2
|
|
952
|
+
.mockReturnValueOnce(new Set(['job1', 'job2', 'job3'])) // job4 depends on all
|
|
953
|
+
|
|
954
|
+
const deployer = new Deployer(deployerOptions)
|
|
955
|
+
await deployer.run()
|
|
956
|
+
|
|
957
|
+
// job1, job2: run on both networks (2 + 2 = 4)
|
|
958
|
+
// job3: only mainnet (1)
|
|
959
|
+
// job4: skip polygon, so only mainnet (1)
|
|
960
|
+
// Total: 6 executions
|
|
961
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(6)
|
|
962
|
+
|
|
963
|
+
// Verify network distribution by checking ExecutionContext constructor calls
|
|
964
|
+
const contextCalls = MockExecutionContext.mock.calls
|
|
965
|
+
const mainnetCalls = contextCalls.filter(call => call[0].chainId === 1)
|
|
966
|
+
const polygonCalls = contextCalls.filter(call => call[0].chainId === 137)
|
|
967
|
+
|
|
968
|
+
expect(mainnetCalls).toHaveLength(4) // All jobs run on mainnet
|
|
969
|
+
expect(polygonCalls).toHaveLength(2) // Only job1 and job2 run on polygon
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
it('should handle partial failure scenario', async () => {
|
|
973
|
+
// Make job2 fail on polygon only
|
|
974
|
+
let callCount = 0
|
|
975
|
+
mockEngine.executeJob.mockImplementation((job, context) => {
|
|
976
|
+
const currentCall = MockExecutionContext.mock.calls[callCount]
|
|
977
|
+
const network = currentCall ? currentCall[0] : null
|
|
978
|
+
callCount++
|
|
979
|
+
|
|
980
|
+
if (job.name === 'job2' && network && network.chainId === 137) {
|
|
981
|
+
throw new Error('Polygon execution failed')
|
|
982
|
+
}
|
|
983
|
+
return Promise.resolve()
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
const deployer = new Deployer(deployerOptions)
|
|
987
|
+
|
|
988
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
989
|
+
|
|
990
|
+
// Should capture the partial failure in output files before throwing
|
|
991
|
+
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
992
|
+
const job2Output = writeFileCalls.find(call =>
|
|
993
|
+
String(call[0]).includes('job2.json')
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
if (job2Output) {
|
|
997
|
+
const job2Content = JSON.parse(job2Output[1] as string)
|
|
998
|
+
const errorEntries = job2Content.networks.filter((entry: any) => entry.status === 'error')
|
|
999
|
+
expect(errorEntries.some((entry: any) =>
|
|
1000
|
+
entry.chainId === '137' && entry.error === 'Polygon execution failed'
|
|
1001
|
+
)).toBe(true)
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
it('should handle context output aggregation correctly', async () => {
|
|
1006
|
+
// Mock different outputs for different networks
|
|
1007
|
+
MockExecutionContext.mockImplementation((network) => ({
|
|
1008
|
+
network,
|
|
1009
|
+
getOutputs: jest.fn().mockReturnValue(new Map<string, any>([
|
|
1010
|
+
[`action.hash`, `0xhash-${network.chainId}`],
|
|
1011
|
+
[`action.receipt`, { status: 1, blockNumber: network.chainId * 100 }]
|
|
1012
|
+
])),
|
|
1013
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
1014
|
+
setOutput: jest.fn(),
|
|
1015
|
+
getOutput: jest.fn()
|
|
1016
|
+
} as any))
|
|
1017
|
+
|
|
1018
|
+
const deployer = new Deployer(deployerOptions)
|
|
1019
|
+
await deployer.run()
|
|
1020
|
+
|
|
1021
|
+
// Verify outputs are correctly segregated by network since they have different outputs
|
|
1022
|
+
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
1023
|
+
const job1Output = writeFileCalls.find(call =>
|
|
1024
|
+
call[0] === '/test/project/output/job1.json'
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
const job1Content = JSON.parse(job1Output![1] as string)
|
|
1028
|
+
// Since outputs differ by network, they should be in separate entries
|
|
1029
|
+
expect(job1Content.networks).toHaveLength(2)
|
|
1030
|
+
|
|
1031
|
+
// Find entries for each network
|
|
1032
|
+
const network1Entry = job1Content.networks.find((entry: any) =>
|
|
1033
|
+
entry.chainIds && entry.chainIds.includes('1')
|
|
1034
|
+
)
|
|
1035
|
+
const network137Entry = job1Content.networks.find((entry: any) =>
|
|
1036
|
+
entry.chainIds && entry.chainIds.includes('137')
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
expect(network1Entry.outputs['action.hash']).toBe('0xhash-1')
|
|
1040
|
+
expect(network137Entry.outputs['action.hash']).toBe('0xhash-137')
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
it('should group networks with identical outputs together', async () => {
|
|
1044
|
+
// Mock identical outputs for different networks
|
|
1045
|
+
MockExecutionContext.mockImplementation(() => ({
|
|
1046
|
+
getOutputs: jest.fn().mockReturnValue(new Map<string, any>([
|
|
1047
|
+
[`contract.address`, `0x1234567890123456789012345678901234567890`],
|
|
1048
|
+
[`contract.txHash`, `0xabcdef1234567890abcdef1234567890abcdef12`]
|
|
1049
|
+
])),
|
|
1050
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
1051
|
+
setOutput: jest.fn(),
|
|
1052
|
+
getOutput: jest.fn()
|
|
1053
|
+
} as any))
|
|
1054
|
+
|
|
1055
|
+
const deployer = new Deployer(deployerOptions)
|
|
1056
|
+
await deployer.run()
|
|
1057
|
+
|
|
1058
|
+
// Verify identical outputs are grouped together
|
|
1059
|
+
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
1060
|
+
const job1Output = writeFileCalls.find(call =>
|
|
1061
|
+
call[0] === '/test/project/output/job1.json'
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
const job1Content = JSON.parse(job1Output![1] as string)
|
|
1065
|
+
// Since outputs are identical, they should be grouped into one entry
|
|
1066
|
+
expect(job1Content.networks).toHaveLength(1)
|
|
1067
|
+
expect(job1Content.networks[0].status).toBe('success')
|
|
1068
|
+
expect(job1Content.networks[0].chainIds).toEqual(['1', '137'])
|
|
1069
|
+
expect(job1Content.networks[0].outputs['contract.address']).toBe('0x1234567890123456789012345678901234567890')
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
it('should handle partial failure scenario with proper grouping', async () => {
|
|
1073
|
+
// Make job1 fail on polygon only
|
|
1074
|
+
let callCount = 0
|
|
1075
|
+
mockEngine.executeJob.mockImplementation((job, context) => {
|
|
1076
|
+
const currentCall = MockExecutionContext.mock.calls[callCount]
|
|
1077
|
+
const network = currentCall ? currentCall[0] : null
|
|
1078
|
+
callCount++
|
|
1079
|
+
|
|
1080
|
+
if (job.name === 'job1' && network && network.chainId === 137) {
|
|
1081
|
+
throw new Error('Polygon execution failed')
|
|
1082
|
+
}
|
|
1083
|
+
return Promise.resolve()
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
// Mock successful outputs for mainnet
|
|
1087
|
+
MockExecutionContext.mockImplementation((network) => ({
|
|
1088
|
+
network,
|
|
1089
|
+
getOutputs: jest.fn().mockReturnValue(new Map<string, any>([
|
|
1090
|
+
[`contract.address`, `0x1234567890123456789012345678901234567890`]
|
|
1091
|
+
])),
|
|
1092
|
+
dispose: jest.fn().mockResolvedValue(undefined),
|
|
1093
|
+
setOutput: jest.fn(),
|
|
1094
|
+
getOutput: jest.fn()
|
|
1095
|
+
} as any))
|
|
1096
|
+
|
|
1097
|
+
const deployer = new Deployer(deployerOptions)
|
|
1098
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
1099
|
+
|
|
1100
|
+
// Verify outputs show both success and error states before throwing
|
|
1101
|
+
const writeFileCalls = mockFs.writeFile.mock.calls
|
|
1102
|
+
const job1Output = writeFileCalls.find(call =>
|
|
1103
|
+
call[0] === '/test/project/output/job1.json'
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
const job1Content = JSON.parse(job1Output![1] as string)
|
|
1107
|
+
expect(job1Content.networks).toHaveLength(2) // One success entry, one error entry
|
|
1108
|
+
|
|
1109
|
+
// Find success and error entries
|
|
1110
|
+
const successEntry = job1Content.networks.find((entry: any) => entry.status === 'success')
|
|
1111
|
+
const errorEntry = job1Content.networks.find((entry: any) => entry.status === 'error')
|
|
1112
|
+
|
|
1113
|
+
expect(successEntry).toBeDefined()
|
|
1114
|
+
expect(successEntry.chainIds).toEqual(['1'])
|
|
1115
|
+
expect(successEntry.outputs['contract.address']).toBe('0x1234567890123456789012345678901234567890')
|
|
1116
|
+
|
|
1117
|
+
expect(errorEntry).toBeDefined()
|
|
1118
|
+
expect(errorEntry.chainId).toBe('137')
|
|
1119
|
+
expect(errorEntry.error).toBe('Polygon execution failed')
|
|
1120
|
+
})
|
|
1121
|
+
})
|
|
1122
|
+
})
|
|
1123
|
+
|
|
1124
|
+
describe('fail-early functionality', () => {
|
|
1125
|
+
beforeEach(() => {
|
|
1126
|
+
// Clear mock call counts for this test suite
|
|
1127
|
+
mockEngine.executeJob.mockClear()
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
it('should stop execution immediately when failEarly is true', async () => {
|
|
1131
|
+
const options: DeployerOptions = {
|
|
1132
|
+
...deployerOptions,
|
|
1133
|
+
runJobs: ['job1'], // Only run job1 to have predictable call count
|
|
1134
|
+
failEarly: true
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Make the first execution fail
|
|
1138
|
+
mockEngine.executeJob.mockRejectedValueOnce(new Error('First job failed'))
|
|
1139
|
+
|
|
1140
|
+
const deployer = new Deployer(options)
|
|
1141
|
+
|
|
1142
|
+
await expect(deployer.run()).rejects.toThrow('First job failed')
|
|
1143
|
+
|
|
1144
|
+
// Should only attempt the first execution, not continue to other networks/jobs
|
|
1145
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(1)
|
|
1146
|
+
})
|
|
1147
|
+
|
|
1148
|
+
it('should continue through all jobs/networks when failEarly is false', async () => {
|
|
1149
|
+
const options: DeployerOptions = {
|
|
1150
|
+
...deployerOptions,
|
|
1151
|
+
runJobs: ['job1'], // Only run job1 to have predictable call count
|
|
1152
|
+
failEarly: false // explicit false
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Make the first execution fail but others succeed
|
|
1156
|
+
mockEngine.executeJob.mockRejectedValueOnce(new Error('First job failed'))
|
|
1157
|
+
mockEngine.executeJob.mockResolvedValue(undefined)
|
|
1158
|
+
|
|
1159
|
+
const deployer = new Deployer(options)
|
|
1160
|
+
|
|
1161
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
1162
|
+
|
|
1163
|
+
// Should attempt all executions (2 networks * 1 job = 2 calls)
|
|
1164
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
it('should default to failEarly: false when option is not provided', async () => {
|
|
1168
|
+
const options: DeployerOptions = {
|
|
1169
|
+
...deployerOptions,
|
|
1170
|
+
runJobs: ['job1'] // Only run job1 to have predictable call count
|
|
1171
|
+
// failEarly not specified, should default to false
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Make the first execution fail but others succeed
|
|
1175
|
+
mockEngine.executeJob.mockRejectedValueOnce(new Error('First job failed'))
|
|
1176
|
+
mockEngine.executeJob.mockResolvedValue(undefined)
|
|
1177
|
+
|
|
1178
|
+
const deployer = new Deployer(options)
|
|
1179
|
+
|
|
1180
|
+
await expect(deployer.run()).rejects.toThrow('One or more jobs failed during execution')
|
|
1181
|
+
|
|
1182
|
+
// Should attempt all executions
|
|
1183
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
it('should not throw when all jobs succeed, regardless of failEarly setting', async () => {
|
|
1187
|
+
const options: DeployerOptions = {
|
|
1188
|
+
...deployerOptions,
|
|
1189
|
+
runJobs: ['job1'], // Only run job1 to have predictable call count
|
|
1190
|
+
failEarly: true
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// All executions succeed
|
|
1194
|
+
mockEngine.executeJob.mockResolvedValue(undefined)
|
|
1195
|
+
|
|
1196
|
+
const deployer = new Deployer(options)
|
|
1197
|
+
|
|
1198
|
+
await expect(deployer.run()).resolves.not.toThrow()
|
|
1199
|
+
|
|
1200
|
+
// Should complete all executions
|
|
1201
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(2)
|
|
1202
|
+
})
|
|
1203
|
+
})
|
|
1204
|
+
})
|