@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.
Files changed (393) hide show
  1. package/.eslintrc.json +29 -0
  2. package/.github/workflows/ci.yml +181 -0
  3. package/CONCEPT.md +24 -0
  4. package/README.md +772 -0
  5. package/contracts/checked-call.huff +65 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +16 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/common.d.ts +11 -0
  11. package/dist/commands/common.d.ts.map +1 -0
  12. package/dist/commands/common.js +73 -0
  13. package/dist/commands/common.js.map +1 -0
  14. package/dist/commands/dry.d.ts +3 -0
  15. package/dist/commands/dry.d.ts.map +1 -0
  16. package/dist/commands/dry.js +171 -0
  17. package/dist/commands/dry.js.map +1 -0
  18. package/dist/commands/etherscan.d.ts +3 -0
  19. package/dist/commands/etherscan.d.ts.map +1 -0
  20. package/dist/commands/etherscan.js +323 -0
  21. package/dist/commands/etherscan.js.map +1 -0
  22. package/dist/commands/index.d.ts +6 -0
  23. package/dist/commands/index.d.ts.map +1 -0
  24. package/dist/commands/index.js +22 -0
  25. package/dist/commands/index.js.map +1 -0
  26. package/dist/commands/list.d.ts +3 -0
  27. package/dist/commands/list.d.ts.map +1 -0
  28. package/dist/commands/list.js +259 -0
  29. package/dist/commands/list.js.map +1 -0
  30. package/dist/commands/run.d.ts +3 -0
  31. package/dist/commands/run.d.ts.map +1 -0
  32. package/dist/commands/run.js +96 -0
  33. package/dist/commands/run.js.map +1 -0
  34. package/dist/commands/utils.d.ts +3 -0
  35. package/dist/commands/utils.d.ts.map +1 -0
  36. package/dist/commands/utils.js +46 -0
  37. package/dist/commands/utils.js.map +1 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +58 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lib/__tests__/deployer-events.spec.d.ts +2 -0
  43. package/dist/lib/__tests__/deployer-events.spec.d.ts.map +1 -0
  44. package/dist/lib/__tests__/deployer-events.spec.js +260 -0
  45. package/dist/lib/__tests__/deployer-events.spec.js.map +1 -0
  46. package/dist/lib/__tests__/deployer.spec.d.ts +2 -0
  47. package/dist/lib/__tests__/deployer.spec.d.ts.map +1 -0
  48. package/dist/lib/__tests__/deployer.spec.js +884 -0
  49. package/dist/lib/__tests__/deployer.spec.js.map +1 -0
  50. package/dist/lib/__tests__/network-utils.spec.d.ts +2 -0
  51. package/dist/lib/__tests__/network-utils.spec.d.ts.map +1 -0
  52. package/dist/lib/__tests__/network-utils.spec.js +140 -0
  53. package/dist/lib/__tests__/network-utils.spec.js.map +1 -0
  54. package/dist/lib/contracts/__tests__/repository.spec.d.ts +2 -0
  55. package/dist/lib/contracts/__tests__/repository.spec.d.ts.map +1 -0
  56. package/dist/lib/contracts/__tests__/repository.spec.js +321 -0
  57. package/dist/lib/contracts/__tests__/repository.spec.js.map +1 -0
  58. package/dist/lib/contracts/repository.d.ts +27 -0
  59. package/dist/lib/contracts/repository.d.ts.map +1 -0
  60. package/dist/lib/contracts/repository.js +241 -0
  61. package/dist/lib/contracts/repository.js.map +1 -0
  62. package/dist/lib/core/__tests__/engine.spec.d.ts +2 -0
  63. package/dist/lib/core/__tests__/engine.spec.d.ts.map +1 -0
  64. package/dist/lib/core/__tests__/engine.spec.js +1212 -0
  65. package/dist/lib/core/__tests__/engine.spec.js.map +1 -0
  66. package/dist/lib/core/__tests__/graph.spec.d.ts +2 -0
  67. package/dist/lib/core/__tests__/graph.spec.d.ts.map +1 -0
  68. package/dist/lib/core/__tests__/graph.spec.js +116 -0
  69. package/dist/lib/core/__tests__/graph.spec.js.map +1 -0
  70. package/dist/lib/core/__tests__/json-integration.spec.d.ts +2 -0
  71. package/dist/lib/core/__tests__/json-integration.spec.d.ts.map +1 -0
  72. package/dist/lib/core/__tests__/json-integration.spec.js +300 -0
  73. package/dist/lib/core/__tests__/json-integration.spec.js.map +1 -0
  74. package/dist/lib/core/__tests__/loader.spec.d.ts +2 -0
  75. package/dist/lib/core/__tests__/loader.spec.d.ts.map +1 -0
  76. package/dist/lib/core/__tests__/loader.spec.js +288 -0
  77. package/dist/lib/core/__tests__/loader.spec.js.map +1 -0
  78. package/dist/lib/core/__tests__/multi-platform-verification.spec.d.ts +2 -0
  79. package/dist/lib/core/__tests__/multi-platform-verification.spec.d.ts.map +1 -0
  80. package/dist/lib/core/__tests__/multi-platform-verification.spec.js +342 -0
  81. package/dist/lib/core/__tests__/multi-platform-verification.spec.js.map +1 -0
  82. package/dist/lib/core/__tests__/resolver.spec.d.ts +2 -0
  83. package/dist/lib/core/__tests__/resolver.spec.d.ts.map +1 -0
  84. package/dist/lib/core/__tests__/resolver.spec.js +1367 -0
  85. package/dist/lib/core/__tests__/resolver.spec.js.map +1 -0
  86. package/dist/lib/core/__tests__/static-action.spec.d.ts +2 -0
  87. package/dist/lib/core/__tests__/static-action.spec.d.ts.map +1 -0
  88. package/dist/lib/core/__tests__/static-action.spec.js +136 -0
  89. package/dist/lib/core/__tests__/static-action.spec.js.map +1 -0
  90. package/dist/lib/core/context.d.ts +29 -0
  91. package/dist/lib/core/context.d.ts.map +1 -0
  92. package/dist/lib/core/context.js +88 -0
  93. package/dist/lib/core/context.js.map +1 -0
  94. package/dist/lib/core/engine.d.ts +25 -0
  95. package/dist/lib/core/engine.d.ts.map +1 -0
  96. package/dist/lib/core/engine.js +1191 -0
  97. package/dist/lib/core/engine.js.map +1 -0
  98. package/dist/lib/core/graph.d.ts +18 -0
  99. package/dist/lib/core/graph.d.ts.map +1 -0
  100. package/dist/lib/core/graph.js +158 -0
  101. package/dist/lib/core/graph.js.map +1 -0
  102. package/dist/lib/core/loader.d.ts +25 -0
  103. package/dist/lib/core/loader.d.ts.map +1 -0
  104. package/dist/lib/core/loader.js +248 -0
  105. package/dist/lib/core/loader.js.map +1 -0
  106. package/dist/lib/core/resolver.d.ts +20 -0
  107. package/dist/lib/core/resolver.d.ts.map +1 -0
  108. package/dist/lib/core/resolver.js +307 -0
  109. package/dist/lib/core/resolver.js.map +1 -0
  110. package/dist/lib/deployer.d.ts +39 -0
  111. package/dist/lib/deployer.d.ts.map +1 -0
  112. package/dist/lib/deployer.js +533 -0
  113. package/dist/lib/deployer.js.map +1 -0
  114. package/dist/lib/events/__tests__/event-system.spec.d.ts +2 -0
  115. package/dist/lib/events/__tests__/event-system.spec.d.ts.map +1 -0
  116. package/dist/lib/events/__tests__/event-system.spec.js +256 -0
  117. package/dist/lib/events/__tests__/event-system.spec.js.map +1 -0
  118. package/dist/lib/events/cli-adapter.d.ts +13 -0
  119. package/dist/lib/events/cli-adapter.d.ts.map +1 -0
  120. package/dist/lib/events/cli-adapter.js +244 -0
  121. package/dist/lib/events/cli-adapter.js.map +1 -0
  122. package/dist/lib/events/emitter.d.ts +11 -0
  123. package/dist/lib/events/emitter.d.ts.map +1 -0
  124. package/dist/lib/events/emitter.js +29 -0
  125. package/dist/lib/events/emitter.js.map +1 -0
  126. package/dist/lib/events/index.d.ts +4 -0
  127. package/dist/lib/events/index.d.ts.map +1 -0
  128. package/dist/lib/events/index.js +20 -0
  129. package/dist/lib/events/index.js.map +1 -0
  130. package/dist/lib/events/types.d.ts +368 -0
  131. package/dist/lib/events/types.d.ts.map +1 -0
  132. package/dist/lib/events/types.js +3 -0
  133. package/dist/lib/events/types.js.map +1 -0
  134. package/dist/lib/index.d.ts +5 -0
  135. package/dist/lib/index.d.ts.map +1 -0
  136. package/dist/lib/index.js +44 -0
  137. package/dist/lib/index.js.map +1 -0
  138. package/dist/lib/network-loader.d.ts +3 -0
  139. package/dist/lib/network-loader.d.ts.map +1 -0
  140. package/dist/lib/network-loader.js +80 -0
  141. package/dist/lib/network-loader.js.map +1 -0
  142. package/dist/lib/network-match.d.ts +3 -0
  143. package/dist/lib/network-match.d.ts.map +1 -0
  144. package/dist/lib/network-match.js +62 -0
  145. package/dist/lib/network-match.js.map +1 -0
  146. package/dist/lib/network-utils.d.ts +4 -0
  147. package/dist/lib/network-utils.d.ts.map +1 -0
  148. package/dist/lib/network-utils.js +39 -0
  149. package/dist/lib/network-utils.js.map +1 -0
  150. package/dist/lib/parsers/__tests__/buildinfo.spec.d.ts +2 -0
  151. package/dist/lib/parsers/__tests__/buildinfo.spec.d.ts.map +1 -0
  152. package/dist/lib/parsers/__tests__/buildinfo.spec.js +132 -0
  153. package/dist/lib/parsers/__tests__/buildinfo.spec.js.map +1 -0
  154. package/dist/lib/parsers/__tests__/job.spec.d.ts +2 -0
  155. package/dist/lib/parsers/__tests__/job.spec.d.ts.map +1 -0
  156. package/dist/lib/parsers/__tests__/job.spec.js +318 -0
  157. package/dist/lib/parsers/__tests__/job.spec.js.map +1 -0
  158. package/dist/lib/parsers/__tests__/template.spec.d.ts +2 -0
  159. package/dist/lib/parsers/__tests__/template.spec.d.ts.map +1 -0
  160. package/dist/lib/parsers/__tests__/template.spec.js +126 -0
  161. package/dist/lib/parsers/__tests__/template.spec.js.map +1 -0
  162. package/dist/lib/parsers/artifact/__tests__/artifact.spec.d.ts +2 -0
  163. package/dist/lib/parsers/artifact/__tests__/artifact.spec.d.ts.map +1 -0
  164. package/dist/lib/parsers/artifact/__tests__/artifact.spec.js +128 -0
  165. package/dist/lib/parsers/artifact/__tests__/artifact.spec.js.map +1 -0
  166. package/dist/lib/parsers/artifact/foundry-1.2.d.ts +3 -0
  167. package/dist/lib/parsers/artifact/foundry-1.2.d.ts.map +1 -0
  168. package/dist/lib/parsers/artifact/foundry-1.2.js +82 -0
  169. package/dist/lib/parsers/artifact/foundry-1.2.js.map +1 -0
  170. package/dist/lib/parsers/artifact/index.d.ts +3 -0
  171. package/dist/lib/parsers/artifact/index.d.ts.map +1 -0
  172. package/dist/lib/parsers/artifact/index.js +17 -0
  173. package/dist/lib/parsers/artifact/index.js.map +1 -0
  174. package/dist/lib/parsers/artifact/types.d.ts +3 -0
  175. package/dist/lib/parsers/artifact/types.d.ts.map +1 -0
  176. package/dist/lib/parsers/artifact/types.js +3 -0
  177. package/dist/lib/parsers/artifact/types.js.map +1 -0
  178. package/dist/lib/parsers/buildinfo.d.ts +5 -0
  179. package/dist/lib/parsers/buildinfo.d.ts.map +1 -0
  180. package/dist/lib/parsers/buildinfo.js +85 -0
  181. package/dist/lib/parsers/buildinfo.js.map +1 -0
  182. package/dist/lib/parsers/constants.d.ts +4 -0
  183. package/dist/lib/parsers/constants.d.ts.map +1 -0
  184. package/dist/lib/parsers/constants.js +45 -0
  185. package/dist/lib/parsers/constants.js.map +1 -0
  186. package/dist/lib/parsers/index.d.ts +5 -0
  187. package/dist/lib/parsers/index.d.ts.map +1 -0
  188. package/dist/lib/parsers/index.js +21 -0
  189. package/dist/lib/parsers/index.js.map +1 -0
  190. package/dist/lib/parsers/job.d.ts +3 -0
  191. package/dist/lib/parsers/job.d.ts.map +1 -0
  192. package/dist/lib/parsers/job.js +74 -0
  193. package/dist/lib/parsers/job.js.map +1 -0
  194. package/dist/lib/parsers/template.d.ts +3 -0
  195. package/dist/lib/parsers/template.d.ts.map +1 -0
  196. package/dist/lib/parsers/template.js +91 -0
  197. package/dist/lib/parsers/template.js.map +1 -0
  198. package/dist/lib/std/templates/assured-deployment.yaml +45 -0
  199. package/dist/lib/std/templates/erc-2470.yaml +67 -0
  200. package/dist/lib/std/templates/min-balance.yaml +32 -0
  201. package/dist/lib/std/templates/nano-universal-deployer.yaml +59 -0
  202. package/dist/lib/std/templates/raw-erc-2470.yaml +59 -0
  203. package/dist/lib/std/templates/raw-nano-universal-deployer.yaml +51 -0
  204. package/dist/lib/std/templates/raw-sequence-universal-deployer-2.yaml +48 -0
  205. package/dist/lib/std/templates/sequence-universal-deployer-2.yaml +57 -0
  206. package/dist/lib/types/__tests__/json-request-action.spec.d.ts +2 -0
  207. package/dist/lib/types/__tests__/json-request-action.spec.d.ts.map +1 -0
  208. package/dist/lib/types/__tests__/json-request-action.spec.js +219 -0
  209. package/dist/lib/types/__tests__/json-request-action.spec.js.map +1 -0
  210. package/dist/lib/types/__tests__/read-json-value.spec.d.ts +2 -0
  211. package/dist/lib/types/__tests__/read-json-value.spec.d.ts.map +1 -0
  212. package/dist/lib/types/__tests__/read-json-value.spec.js +233 -0
  213. package/dist/lib/types/__tests__/read-json-value.spec.js.map +1 -0
  214. package/dist/lib/types/actions.d.ts +74 -0
  215. package/dist/lib/types/actions.d.ts.map +1 -0
  216. package/dist/lib/types/actions.js +18 -0
  217. package/dist/lib/types/actions.js.map +1 -0
  218. package/dist/lib/types/artifacts.d.ts +15 -0
  219. package/dist/lib/types/artifacts.d.ts.map +1 -0
  220. package/dist/lib/types/artifacts.js +3 -0
  221. package/dist/lib/types/artifacts.js.map +1 -0
  222. package/dist/lib/types/buildinfo.d.ts +112 -0
  223. package/dist/lib/types/buildinfo.d.ts.map +1 -0
  224. package/dist/lib/types/buildinfo.js +3 -0
  225. package/dist/lib/types/buildinfo.js.map +1 -0
  226. package/dist/lib/types/conditions.d.ts +17 -0
  227. package/dist/lib/types/conditions.d.ts.map +1 -0
  228. package/dist/lib/types/conditions.js +21 -0
  229. package/dist/lib/types/conditions.js.map +1 -0
  230. package/dist/lib/types/contracts.d.ts +14 -0
  231. package/dist/lib/types/contracts.d.ts.map +1 -0
  232. package/dist/lib/types/contracts.js +3 -0
  233. package/dist/lib/types/contracts.js.map +1 -0
  234. package/dist/lib/types/definitions.d.ts +51 -0
  235. package/dist/lib/types/definitions.d.ts.map +1 -0
  236. package/dist/lib/types/definitions.js +3 -0
  237. package/dist/lib/types/definitions.js.map +1 -0
  238. package/dist/lib/types/index.d.ts +9 -0
  239. package/dist/lib/types/index.d.ts.map +1 -0
  240. package/dist/lib/types/index.js +25 -0
  241. package/dist/lib/types/index.js.map +1 -0
  242. package/dist/lib/types/network.d.ts +9 -0
  243. package/dist/lib/types/network.d.ts.map +1 -0
  244. package/dist/lib/types/network.js +3 -0
  245. package/dist/lib/types/network.js.map +1 -0
  246. package/dist/lib/types/project.d.ts +5 -0
  247. package/dist/lib/types/project.d.ts.map +1 -0
  248. package/dist/lib/types/project.js +3 -0
  249. package/dist/lib/types/project.js.map +1 -0
  250. package/dist/lib/types/task.d.ts +9 -0
  251. package/dist/lib/types/task.d.ts.map +1 -0
  252. package/dist/lib/types/task.js +3 -0
  253. package/dist/lib/types/task.js.map +1 -0
  254. package/dist/lib/types/values.d.ts +78 -0
  255. package/dist/lib/types/values.d.ts.map +1 -0
  256. package/dist/lib/types/values.js +3 -0
  257. package/dist/lib/types/values.js.map +1 -0
  258. package/dist/lib/utils/validation.d.ts +5 -0
  259. package/dist/lib/utils/validation.d.ts.map +1 -0
  260. package/dist/lib/utils/validation.js +77 -0
  261. package/dist/lib/utils/validation.js.map +1 -0
  262. package/dist/lib/validation/contract-references.d.ts +12 -0
  263. package/dist/lib/validation/contract-references.d.ts.map +1 -0
  264. package/dist/lib/validation/contract-references.js +112 -0
  265. package/dist/lib/validation/contract-references.js.map +1 -0
  266. package/dist/lib/validation/index.d.ts +1 -0
  267. package/dist/lib/validation/index.d.ts.map +1 -0
  268. package/dist/lib/validation/index.js +2 -0
  269. package/dist/lib/validation/index.js.map +1 -0
  270. package/dist/lib/verification/__tests__/etherscan.spec.d.ts +2 -0
  271. package/dist/lib/verification/__tests__/etherscan.spec.d.ts.map +1 -0
  272. package/dist/lib/verification/__tests__/etherscan.spec.js +565 -0
  273. package/dist/lib/verification/__tests__/etherscan.spec.js.map +1 -0
  274. package/dist/lib/verification/__tests__/sourcify.spec.d.ts +2 -0
  275. package/dist/lib/verification/__tests__/sourcify.spec.d.ts.map +1 -0
  276. package/dist/lib/verification/__tests__/sourcify.spec.js +212 -0
  277. package/dist/lib/verification/__tests__/sourcify.spec.js.map +1 -0
  278. package/dist/lib/verification/etherscan.d.ts +56 -0
  279. package/dist/lib/verification/etherscan.d.ts.map +1 -0
  280. package/dist/lib/verification/etherscan.js +340 -0
  281. package/dist/lib/verification/etherscan.js.map +1 -0
  282. package/dist/lib/verification/sourcify.d.ts +12 -0
  283. package/dist/lib/verification/sourcify.d.ts.map +1 -0
  284. package/dist/lib/verification/sourcify.js +227 -0
  285. package/dist/lib/verification/sourcify.js.map +1 -0
  286. package/eslint.config.js +48 -0
  287. package/examples/jobs/guards-v1.yaml +17 -0
  288. package/examples/jobs/sequence-seq-0001-patch.yaml +59 -0
  289. package/examples/jobs/sequence-v1.yaml +59 -0
  290. package/examples/templates/sequence-factory-v1.yaml +56 -0
  291. package/jest.config.js +25 -0
  292. package/package.json +68 -0
  293. package/src/cli.ts +17 -0
  294. package/src/commands/common.ts +61 -0
  295. package/src/commands/dry.ts +208 -0
  296. package/src/commands/etherscan.ts +360 -0
  297. package/src/commands/index.ts +5 -0
  298. package/src/commands/list.ts +249 -0
  299. package/src/commands/run.ts +136 -0
  300. package/src/commands/utils.ts +52 -0
  301. package/src/index.ts +67 -0
  302. package/src/lib/__tests__/deployer-events.spec.ts +338 -0
  303. package/src/lib/__tests__/deployer.spec.ts +1204 -0
  304. package/src/lib/__tests__/network-utils.spec.ts +181 -0
  305. package/src/lib/artifacts/__tests__/fixtures/contract1.json +19 -0
  306. package/src/lib/artifacts/__tests__/fixtures/contract2.json +19 -0
  307. package/src/lib/artifacts/__tests__/fixtures/duplicate-name.json +19 -0
  308. package/src/lib/artifacts/__tests__/fixtures/nested/nested-contract.json +18 -0
  309. package/src/lib/artifacts/__tests__/fixtures/not-an-artifact.json +8 -0
  310. package/src/lib/artifacts/__tests__/fixtures/readme.txt +2 -0
  311. package/src/lib/contracts/__tests__/repository.spec.ts +344 -0
  312. package/src/lib/contracts/repository.ts +313 -0
  313. package/src/lib/core/__tests__/engine.spec.ts +1514 -0
  314. package/src/lib/core/__tests__/graph.spec.ts +125 -0
  315. package/src/lib/core/__tests__/json-integration.spec.ts +360 -0
  316. package/src/lib/core/__tests__/loader.spec.ts +334 -0
  317. package/src/lib/core/__tests__/multi-platform-verification.spec.ts +406 -0
  318. package/src/lib/core/__tests__/resolver.spec.ts +1693 -0
  319. package/src/lib/core/__tests__/static-action.spec.ts +172 -0
  320. package/src/lib/core/context.ts +127 -0
  321. package/src/lib/core/engine.ts +1531 -0
  322. package/src/lib/core/graph.ts +252 -0
  323. package/src/lib/core/loader.ts +263 -0
  324. package/src/lib/core/resolver.ts +498 -0
  325. package/src/lib/deployer.ts +768 -0
  326. package/src/lib/events/__tests__/event-system.spec.ts +343 -0
  327. package/src/lib/events/cli-adapter.ts +325 -0
  328. package/src/lib/events/emitter.ts +62 -0
  329. package/src/lib/events/index.ts +3 -0
  330. package/src/lib/events/types.ts +469 -0
  331. package/src/lib/index.ts +14 -0
  332. package/src/lib/network-loader.ts +59 -0
  333. package/src/lib/network-utils.ts +64 -0
  334. package/src/lib/parsers/__tests__/buildinfo.spec.ts +122 -0
  335. package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-bytecode-buildinfo.json +62 -0
  336. package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-json.txt +2 -0
  337. package/src/lib/parsers/__tests__/fixtures/buildinfo/multi-contract-buildinfo.json +89 -0
  338. package/src/lib/parsers/__tests__/fixtures/buildinfo/no-contracts-buildinfo.json +17 -0
  339. package/src/lib/parsers/__tests__/fixtures/buildinfo/simple-buildinfo.json +63 -0
  340. package/src/lib/parsers/__tests__/fixtures/buildinfo/wrong-format.json +4 -0
  341. package/src/lib/parsers/__tests__/job.spec.ts +335 -0
  342. package/src/lib/parsers/__tests__/template.spec.ts +111 -0
  343. package/src/lib/parsers/artifact/__tests__/artifact.spec.ts +117 -0
  344. package/src/lib/parsers/artifact/__tests__/fixtures/empty-bytecode.json +5 -0
  345. package/src/lib/parsers/artifact/__tests__/fixtures/hardhat-artifact.json +67 -0
  346. package/src/lib/parsers/artifact/__tests__/fixtures/invalid-bytecode.json +5 -0
  347. package/src/lib/parsers/artifact/__tests__/fixtures/invalid-json.txt +11 -0
  348. package/src/lib/parsers/artifact/__tests__/fixtures/minimal-artifact.json +5 -0
  349. package/src/lib/parsers/artifact/__tests__/fixtures/missing-abi.json +4 -0
  350. package/src/lib/parsers/artifact/__tests__/fixtures/missing-bytecode.json +11 -0
  351. package/src/lib/parsers/artifact/__tests__/fixtures/missing-contract-name.json +11 -0
  352. package/src/lib/parsers/artifact/__tests__/fixtures/simple-artifact.json +40 -0
  353. package/src/lib/parsers/artifact/__tests__/fixtures/wrong-types.json +7 -0
  354. package/src/lib/parsers/artifact/foundry-1.2.ts +72 -0
  355. package/src/lib/parsers/artifact/index.ts +27 -0
  356. package/src/lib/parsers/artifact/types.ts +9 -0
  357. package/src/lib/parsers/buildinfo.ts +127 -0
  358. package/src/lib/parsers/constants.ts +56 -0
  359. package/src/lib/parsers/index.ts +5 -0
  360. package/src/lib/parsers/job.ts +101 -0
  361. package/src/lib/parsers/template.ts +131 -0
  362. package/src/lib/std/templates/assured-deployment.yaml +45 -0
  363. package/src/lib/std/templates/erc-2470.yaml +67 -0
  364. package/src/lib/std/templates/min-balance.yaml +32 -0
  365. package/src/lib/std/templates/nano-universal-deployer.yaml +59 -0
  366. package/src/lib/std/templates/raw-erc-2470.yaml +59 -0
  367. package/src/lib/std/templates/raw-nano-universal-deployer.yaml +51 -0
  368. package/src/lib/std/templates/raw-sequence-universal-deployer-2.yaml +48 -0
  369. package/src/lib/std/templates/sequence-universal-deployer-2.yaml +57 -0
  370. package/src/lib/types/__tests__/json-request-action.spec.ts +243 -0
  371. package/src/lib/types/__tests__/read-json-value.spec.ts +264 -0
  372. package/src/lib/types/actions.ts +127 -0
  373. package/src/lib/types/artifacts.ts +21 -0
  374. package/src/lib/types/buildinfo.ts +116 -0
  375. package/src/lib/types/conditions.ts +50 -0
  376. package/src/lib/types/contracts.ts +23 -0
  377. package/src/lib/types/definitions.ts +68 -0
  378. package/src/lib/types/index.ts +8 -0
  379. package/src/lib/types/network.ts +22 -0
  380. package/src/lib/types/project.ts +9 -0
  381. package/src/lib/types/task.ts +9 -0
  382. package/src/lib/types/values.ts +116 -0
  383. package/src/lib/utils/validation.ts +116 -0
  384. package/src/lib/validation/contract-references.ts +210 -0
  385. package/src/lib/validation/index.ts +1 -0
  386. package/src/lib/verification/__tests__/etherscan.spec.ts +710 -0
  387. package/src/lib/verification/__tests__/sourcify.spec.ts +288 -0
  388. package/src/lib/verification/etherscan.ts +546 -0
  389. package/src/lib/verification/sourcify.ts +248 -0
  390. package/test_validation/artifacts/TestContract.json +9 -0
  391. package/test_validation/jobs/test-missing.yaml +16 -0
  392. package/test_validation/networks.yaml +3 -0
  393. 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
+ })