@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,1514 @@
1
+ import { ethers } from 'ethers'
2
+ import { ExecutionEngine } from '../engine'
3
+ import { ExecutionContext } from '../context'
4
+ import { ContractRepository } from '../../contracts/repository'
5
+ import { Job, Template, JobAction, Action, Network } from '../../types'
6
+ import { VerificationPlatformRegistry } from '../../verification/etherscan'
7
+
8
+ // Test constants
9
+ const TEST_ADDRESSES = {
10
+ // Standard anvil addresses for testing
11
+ DEPLOYER: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', // First anvil account
12
+ RECIPIENT_1: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // Second anvil account
13
+ RECIPIENT_2: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', // Third anvil account
14
+ RECIPIENT_3: '0x90F79bf6EB2c4f870365E785982E1f101E93b906', // Fourth anvil account
15
+ RECIPIENT_4: '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65', // Fifth anvil account
16
+ RECIPIENT_5: '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc', // Sixth anvil account
17
+ CONTRACT_ADDRESS: '0x5FbDB2315678afecb367f032d93F642f64180aa3', // Standard contract address
18
+ DUMMY_ADDRESS: '0x1234567890123456789012345678901234567890' // For mock tests
19
+ } as const
20
+
21
+ const TEST_BYTECODES = {
22
+ // Valid contract bytecode that works for all tests
23
+ SIMPLE_CONTRACT: '0x6080604052348015600e575f5ffd5b5060c180601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c806390c52443146034578063d09de08a14604d575b5f5ffd5b603b5f5481565b60405190815260200160405180910390f35b60536055565b005b5f805490806061836068565b9190505550565b5f60018201608457634e487b7160e01b5f52601160045260245ffd5b506001019056fea264697066735822122061c8cc43c72d6b23b16f7a7337dd15b93d71eb94a9d5247911e39f486e1f94f964736f6c634300081e0033',
24
+ // Simple contract that returns 42
25
+ SIMPLE_RETURN_42: '0x6080604052348015600f57600080fd5b5060b68061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063a87d942c14602d575b600080fd5b60336035565b005b6000602a9050909156fea2646970667358221220d1b0e2d6c9f3e8a6f5b8d2e3a4c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c556',
26
+ // Mini contract with multiplication function
27
+ MINI_CONTRACT: '0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80636df5b97a14610038578063f8a8fd6d14610068575b5f5ffd5b610052600480360381019061004d91906100da565b610086565b60405161005f9190610127565b60405180910390f35b61007061009b565b60405161007d9190610127565b60405180910390f35b5f8183610093919061016d565b905092915050565b5f602a905090565b5f5ffd5b5f819050919050565b6100b9816100a7565b81146100c3575f5ffd5b50565b5f813590506100d4816100b0565b92915050565b5f5f604083850312156100f0576100ef6100a3565b5b5f6100fd858286016100c6565b925050602061010e858286016100c6565b9150509250929050565b610121816100a7565b82525050565b5f60208201905061013a5f830184610118565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610177826100a7565b9150610182836100a7565b9250828202610190816100a7565b915082820484148315176101a7576101a6610140565b5b509291505056fea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033',
28
+ // Minimal deployment bytecode
29
+ MINIMAL_DEPLOY: '0x608060405234801561000f575f5ffd5b50603e80601c5f395ff3fe60806040525f80fdfea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033'
30
+ } as const
31
+
32
+ const TEST_VALUES = {
33
+ ONE_ETH: '1000000000000000000',
34
+ HALF_ETH: '500000000000000000',
35
+ TWO_ETH: '2000000000000000000',
36
+ SMALL_AMOUNT: '1000000000000000000' // 1 ETH for testing
37
+ } as const
38
+
39
+ const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' // First anvil account
40
+
41
+ describe('ExecutionEngine', () => {
42
+ let engine: ExecutionEngine
43
+ let context: ExecutionContext
44
+ let mockNetwork: Network
45
+ let mockRegistry: ContractRepository
46
+ let templates: Map<string, Template>
47
+ let anvilProvider: ethers.JsonRpcProvider
48
+
49
+ beforeAll(async () => {
50
+ // Allow configuring RPC URL via environment variable for CI
51
+ const rpcUrl = process.env.RPC_URL || 'http://127.0.0.1:8545'
52
+ mockNetwork = { name: 'testnet', chainId: 999, rpcUrl }
53
+
54
+ // Try to connect to the node, fail immediately if not available
55
+ const provider = new ethers.JsonRpcProvider(rpcUrl)
56
+ await provider.getNetwork()
57
+ })
58
+
59
+ beforeEach(async () => {
60
+ const rpcUrl = process.env.RPC_URL || 'http://127.0.0.1:8545'
61
+ anvilProvider = new ethers.JsonRpcProvider(rpcUrl)
62
+
63
+ mockRegistry = new ContractRepository()
64
+ context = new ExecutionContext(mockNetwork, TEST_PRIVATE_KEY, mockRegistry)
65
+
66
+ // Initialize templates map
67
+ templates = new Map()
68
+
69
+ // Create empty verification registry for tests
70
+ const verificationRegistry = new VerificationPlatformRegistry()
71
+ engine = new ExecutionEngine(templates, undefined, verificationRegistry)
72
+ })
73
+
74
+ afterEach(async () => {
75
+ // Clean up providers to prevent hanging connections
76
+ if (anvilProvider) {
77
+ try {
78
+ if (anvilProvider.destroy) {
79
+ await anvilProvider.destroy()
80
+ }
81
+ } catch (error) {
82
+ // Ignore cleanup errors
83
+ }
84
+ }
85
+
86
+ if (context) {
87
+ try {
88
+ await context.dispose()
89
+ } catch (error) {
90
+ // Ignore cleanup errors
91
+ }
92
+ }
93
+ })
94
+
95
+ describe('executeJob', () => {
96
+ it('should execute a simple job with no dependencies', async () => {
97
+ const job: Job = {
98
+ name: 'simple-job',
99
+ version: '1.0.0',
100
+ actions: [
101
+ {
102
+ name: 'send-eth',
103
+ template: 'send-transaction',
104
+ arguments: {
105
+ to: TEST_ADDRESSES.RECIPIENT_1,
106
+ value: TEST_VALUES.ONE_ETH,
107
+ data: '0x'
108
+ }
109
+ }
110
+ ]
111
+ }
112
+
113
+ await expect(engine.executeJob(job, context)).resolves.not.toThrow()
114
+
115
+ // Check that output was stored
116
+ expect(context.getOutput('send-eth.hash')).toBeDefined()
117
+ expect(context.getOutput('send-eth.receipt')).toBeDefined()
118
+ })
119
+
120
+ it('should execute actions in dependency order', async () => {
121
+ const executionOrder: string[] = []
122
+
123
+ // Mock the executeAction method to track execution order
124
+ const originalExecuteAction = (engine as any).executeAction
125
+ ;(engine as any).executeAction = async function(action: JobAction | Action, ctx: ExecutionContext, scope: any) {
126
+ const actionName = 'name' in action ? action.name : action.type
127
+ if (actionName) {
128
+ executionOrder.push(actionName)
129
+ }
130
+ // For this test, just track execution - don't actually execute
131
+ }
132
+
133
+ const job: Job = {
134
+ name: 'dependency-job',
135
+ version: '1.0.0',
136
+ actions: [
137
+ {
138
+ name: 'action-c',
139
+ template: 'send-transaction',
140
+ arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
141
+ depends_on: ['action-a', 'action-b']
142
+ },
143
+ {
144
+ name: 'action-a',
145
+ template: 'send-transaction',
146
+ arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' }
147
+ },
148
+ {
149
+ name: 'action-b',
150
+ template: 'send-transaction',
151
+ arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
152
+ depends_on: ['action-a']
153
+ }
154
+ ]
155
+ }
156
+
157
+ await engine.executeJob(job, context)
158
+
159
+ // Restore original method
160
+ ;(engine as any).executeAction = originalExecuteAction
161
+
162
+ expect(executionOrder).toEqual(['action-a', 'action-b', 'action-c'])
163
+ })
164
+
165
+ it('should throw on circular dependencies within a job', async () => {
166
+ const job: Job = {
167
+ name: 'circular-job',
168
+ version: '1.0.0',
169
+ actions: [
170
+ {
171
+ name: 'action-a',
172
+ template: 'send-transaction',
173
+ arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
174
+ depends_on: ['action-b']
175
+ },
176
+ {
177
+ name: 'action-b',
178
+ template: 'send-transaction',
179
+ arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
180
+ depends_on: ['action-a']
181
+ }
182
+ ]
183
+ }
184
+
185
+ await expect(engine.executeJob(job, context)).rejects.toThrow('Circular dependency detected among actions in job "circular-job".')
186
+ })
187
+
188
+ it('should throw on invalid dependencies within a job', async () => {
189
+ const job: Job = {
190
+ name: 'invalid-dep-job',
191
+ version: '1.0.0',
192
+ actions: [
193
+ {
194
+ name: 'action-a',
195
+ template: 'send-transaction',
196
+ arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
197
+ depends_on: ['non-existent-action']
198
+ }
199
+ ]
200
+ }
201
+
202
+ await expect(engine.executeJob(job, context)).rejects.toThrow('Action "action-a" in job "invalid-dep-job" has an invalid dependency on "non-existent-action", which does not exist.')
203
+ })
204
+ })
205
+
206
+ describe('executeAction', () => {
207
+ it('should skip action when skip condition is met', async () => {
208
+ // Set up context to make condition true
209
+ context.setOutput('should_skip', 1)
210
+
211
+ const action: JobAction = {
212
+ name: 'skipped-action',
213
+ template: 'send-transaction',
214
+ arguments: { to: TEST_ADDRESSES.DUMMY_ADDRESS, data: '0x' },
215
+ skip_condition: [{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }]
216
+ }
217
+
218
+ await (engine as any).executeAction(action, context, new Map())
219
+
220
+ // Should not have any outputs since it was skipped
221
+ expect(() => context.getOutput('skipped-action.hash')).toThrow()
222
+ })
223
+
224
+ it('should execute action when skip condition is not met', async () => {
225
+ context.setOutput('should_skip', 0)
226
+
227
+ const action: JobAction = {
228
+ name: 'executed-action',
229
+ template: 'send-transaction',
230
+ arguments: {
231
+ to: TEST_ADDRESSES.RECIPIENT_1,
232
+ value: TEST_VALUES.ONE_ETH,
233
+ data: '0x'
234
+ },
235
+ skip_condition: [{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }]
236
+ }
237
+
238
+ await (engine as any).executeAction(action, context, new Map())
239
+
240
+ // Should have outputs since it was executed
241
+ expect(context.getOutput('executed-action.hash')).toBeDefined()
242
+ })
243
+
244
+ it('should call executeTemplate for template actions', async () => {
245
+ const template: Template = {
246
+ name: 'test-template',
247
+ actions: [
248
+ {
249
+ type: 'send-transaction',
250
+ name: 'param-action',
251
+ arguments: {
252
+ to: TEST_ADDRESSES.RECIPIENT_1,
253
+ value: TEST_VALUES.ONE_ETH,
254
+ data: '0x'
255
+ }
256
+ }
257
+ ]
258
+ }
259
+ templates.set('test-template', template)
260
+
261
+ const action: JobAction = {
262
+ name: 'template-action',
263
+ template: 'test-template',
264
+ arguments: {}
265
+ }
266
+
267
+ await (engine as any).executeAction(action, context, new Map())
268
+ // If no error thrown, template was found and executed
269
+ })
270
+
271
+ it('should call executePrimitive for primitive actions', async () => {
272
+ const action: Action = {
273
+ type: 'send-transaction',
274
+ name: 'primitive-action',
275
+ arguments: {
276
+ to: TEST_ADDRESSES.RECIPIENT_1,
277
+ value: TEST_VALUES.ONE_ETH,
278
+ data: '0x'
279
+ }
280
+ }
281
+
282
+ await (engine as any).executeAction(action, context, new Map())
283
+
284
+ expect(context.getOutput('primitive-action.hash')).toBeDefined()
285
+ })
286
+ })
287
+
288
+ describe('executeTemplate', () => {
289
+ it('should execute template with setup block', async () => {
290
+ // Mock the executeAction method to track execution order instead of sending real transactions
291
+ const executedActions: string[] = []
292
+ const originalExecuteAction = (engine as any).executeAction
293
+ ;(engine as any).executeAction = async function(action: JobAction | Action, ctx: ExecutionContext, scope: any) {
294
+ const actionName = 'name' in action ? action.name : action.type
295
+ if (actionName) {
296
+ executedActions.push(actionName)
297
+ // Mock successful execution by setting outputs
298
+ if (actionName === 'setup-action') {
299
+ ctx.setOutput('setup-action.hash', 'mock-setup-hash')
300
+ ctx.setOutput('setup-action.receipt', { status: 1, blockNumber: 100 })
301
+ } else if (actionName === 'main-action') {
302
+ ctx.setOutput('main-action.hash', 'mock-main-hash')
303
+ ctx.setOutput('main-action.receipt', { status: 1, blockNumber: 101 })
304
+ }
305
+ }
306
+ }
307
+
308
+ const template: Template = {
309
+ name: 'template-with-setup',
310
+ setup: {
311
+ actions: [
312
+ {
313
+ type: 'send-transaction',
314
+ name: 'setup-action',
315
+ arguments: {
316
+ to: TEST_ADDRESSES.RECIPIENT_2,
317
+ value: TEST_VALUES.HALF_ETH,
318
+ data: '0x'
319
+ }
320
+ }
321
+ ]
322
+ },
323
+ actions: [
324
+ {
325
+ type: 'send-transaction',
326
+ name: 'main-action',
327
+ arguments: {
328
+ to: TEST_ADDRESSES.RECIPIENT_3,
329
+ value: TEST_VALUES.ONE_ETH,
330
+ data: '0x'
331
+ }
332
+ }
333
+ ]
334
+ }
335
+ templates.set('template-with-setup', template)
336
+
337
+ const callingAction: JobAction = {
338
+ name: 'test-call',
339
+ template: 'template-with-setup',
340
+ arguments: {}
341
+ }
342
+
343
+ await (engine as any).executeTemplate(callingAction, 'template-with-setup', context)
344
+
345
+ // Restore original method
346
+ ;(engine as any).executeAction = originalExecuteAction
347
+
348
+ // Verify setup action executed before main action
349
+ expect(executedActions).toEqual(['setup-action', 'main-action'])
350
+
351
+ // Both setup and main actions should have executed
352
+ expect(context.getOutput('setup-action.hash')).toBeDefined()
353
+ expect(context.getOutput('main-action.hash')).toBeDefined()
354
+ })
355
+
356
+ it('should skip template actions when template skip condition is met', async () => {
357
+ context.setOutput('skip_template', 1)
358
+
359
+ const template: Template = {
360
+ name: 'skippable-template',
361
+ skip_condition: [{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{skip_template}}', 1] } }],
362
+ actions: [
363
+ {
364
+ type: 'send-transaction',
365
+ name: 'skipped-main-action',
366
+ arguments: {
367
+ to: TEST_ADDRESSES.RECIPIENT_1,
368
+ value: TEST_VALUES.ONE_ETH,
369
+ data: '0x'
370
+ }
371
+ }
372
+ ]
373
+ }
374
+ templates.set('skippable-template', template)
375
+
376
+ const callingAction: JobAction = {
377
+ name: 'test-call',
378
+ template: 'skippable-template',
379
+ arguments: {}
380
+ }
381
+
382
+ await (engine as any).executeTemplate(callingAction, 'skippable-template', context)
383
+
384
+ // Main action should not have executed
385
+ expect(() => context.getOutput('skipped-main-action.hash')).toThrow()
386
+ })
387
+
388
+ it('should skip template actions when setup skip condition is met', async () => {
389
+ context.setOutput('skip_setup', 1)
390
+
391
+ const template: Template = {
392
+ name: 'skippable-setup-template',
393
+ setup: {
394
+ skip_condition: [{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{skip_setup}}', 1] } }],
395
+ actions: [
396
+ {
397
+ type: 'send-transaction',
398
+ name: 'setup-action',
399
+ arguments: {
400
+ to: TEST_ADDRESSES.RECIPIENT_4,
401
+ value: TEST_VALUES.ONE_ETH,
402
+ data: '0x'
403
+ }
404
+ }
405
+ ]
406
+ },
407
+ actions: [
408
+ {
409
+ type: 'send-transaction',
410
+ name: 'main-action-after-skipped-setup',
411
+ arguments: {
412
+ to: TEST_ADDRESSES.RECIPIENT_1,
413
+ value: TEST_VALUES.ONE_ETH,
414
+ data: '0x'
415
+ }
416
+ }
417
+ ]
418
+ }
419
+ templates.set('skippable-setup-template', template)
420
+
421
+ const callingAction: JobAction = {
422
+ name: 'test-call',
423
+ template: 'skippable-setup-template',
424
+ arguments: {}
425
+ }
426
+
427
+ await (engine as any).executeTemplate(callingAction, 'skippable-setup-template', context)
428
+
429
+ // Setup action should have been skipped due to setup skip condition
430
+ expect(() => context.getOutput('setup-action.hash')).toThrow()
431
+ // Main action should still have executed (setup skip conditions don't affect main actions)
432
+ expect(context.getOutput('main-action-after-skipped-setup.hash')).toBeDefined()
433
+ })
434
+
435
+ it('should pass arguments to template and resolve outputs', async () => {
436
+ const template: Template = {
437
+ name: 'parameterized-template',
438
+ actions: [
439
+ {
440
+ type: 'send-transaction',
441
+ name: 'param-action',
442
+ arguments: {
443
+ to: '{{target_address}}',
444
+ value: '{{amount}}',
445
+ data: '0x'
446
+ }
447
+ }
448
+ ],
449
+ outputs: {
450
+ transaction_hash: '{{param-action.hash}}',
451
+ doubled_amount: { type: 'basic-arithmetic', arguments: { operation: 'mul', values: ['{{amount}}', 2] } }
452
+ }
453
+ }
454
+ templates.set('parameterized-template', template)
455
+
456
+ const callingAction: JobAction = {
457
+ name: 'test-param-call',
458
+ template: 'parameterized-template',
459
+ arguments: {
460
+ target_address: TEST_ADDRESSES.RECIPIENT_1,
461
+ amount: TEST_VALUES.ONE_ETH
462
+ }
463
+ }
464
+
465
+ await (engine as any).executeTemplate(callingAction, 'parameterized-template', context)
466
+
467
+ // Check that outputs were stored with the calling action name
468
+ expect(context.getOutput('test-param-call.transaction_hash')).toBeDefined()
469
+ expect(context.getOutput('test-param-call.doubled_amount')).toBe('2000000000000000000')
470
+ })
471
+
472
+ it('should allow job action custom output map to override template outputs', async () => {
473
+ // Mock executeAction so the inner action sets expected outputs
474
+ const originalExecuteAction = (engine as any).executeAction
475
+ ;(engine as any).executeAction = async function(action: JobAction | Action, ctx: ExecutionContext, scope: any) {
476
+ const actionName = 'name' in action ? action.name : action.type
477
+ if (actionName === 'param-action') {
478
+ ctx.setOutput('param-action.hash', '0xhash123')
479
+ ctx.setOutput('param-action.receipt', { status: 1, blockNumber: 111 })
480
+ }
481
+ }
482
+
483
+ const template: Template = {
484
+ name: 'tpl-custom-output',
485
+ actions: [
486
+ {
487
+ type: 'send-transaction',
488
+ name: 'param-action',
489
+ arguments: {
490
+ to: TEST_ADDRESSES.RECIPIENT_1,
491
+ value: TEST_VALUES.ONE_ETH,
492
+ data: '0x'
493
+ }
494
+ }
495
+ ],
496
+ outputs: {
497
+ transaction_hash: '{{param-action.hash}}',
498
+ receipt_block: '{{param-action.receipt.blockNumber}}'
499
+ }
500
+ }
501
+ templates.set('tpl-custom-output', template)
502
+
503
+ const callingAction: JobAction = {
504
+ name: 'custom-call',
505
+ template: 'tpl-custom-output',
506
+ arguments: {},
507
+ // Custom output overrides template outputs
508
+ output: {
509
+ myHash: '{{param-action.hash}}',
510
+ staticValue: '42'
511
+ } as any
512
+ }
513
+
514
+ await (engine as any).executeTemplate(callingAction, 'tpl-custom-output', context)
515
+
516
+ // Restore original method
517
+ ;(engine as any).executeAction = originalExecuteAction
518
+
519
+ // Expect only custom outputs to be present for action name "custom-call"
520
+ expect(context.getOutput('custom-call.myHash')).toBe('0xhash123')
521
+ expect(context.getOutput('custom-call.staticValue')).toBe('42')
522
+
523
+ // Template outputs should NOT be set since custom output overrides them
524
+ expect(() => context.getOutput('custom-call.transaction_hash')).toThrow()
525
+ expect(() => context.getOutput('custom-call.receipt_block')).toThrow()
526
+ })
527
+
528
+ it('should throw when template is not found', async () => {
529
+ const callingAction: JobAction = {
530
+ name: 'test-call',
531
+ template: 'non-existent-template',
532
+ arguments: {}
533
+ }
534
+
535
+ await expect((engine as any).executeTemplate(callingAction, 'non-existent-template', context))
536
+ .rejects.toThrow('Template "non-existent-template" not found')
537
+ })
538
+ })
539
+
540
+ describe('executePrimitive', () => {
541
+ describe('send-transaction', () => {
542
+ it('should send a transaction successfully', async () => {
543
+ const action: Action = {
544
+ type: 'send-transaction',
545
+ name: 'test-tx',
546
+ arguments: {
547
+ to: TEST_ADDRESSES.RECIPIENT_1,
548
+ value: TEST_VALUES.ONE_ETH,
549
+ data: '0x'
550
+ }
551
+ }
552
+
553
+ await (engine as any).executePrimitive(action, context, new Map())
554
+
555
+ const hash = context.getOutput('test-tx.hash')
556
+ const receipt = context.getOutput('test-tx.receipt')
557
+
558
+ expect(hash).toBeDefined()
559
+ expect(receipt).toBeDefined()
560
+ expect(receipt.status).toBe(1)
561
+ })
562
+
563
+ it('should send transaction with resolved arguments', async () => {
564
+ context.setOutput('recipient', TEST_ADDRESSES.RECIPIENT_1)
565
+ context.setOutput('amount', TEST_VALUES.ONE_ETH)
566
+
567
+ const action: Action = {
568
+ type: 'send-transaction',
569
+ name: 'resolved-tx',
570
+ arguments: {
571
+ to: '{{recipient}}',
572
+ value: '{{amount}}',
573
+ data: '0x1234'
574
+ }
575
+ }
576
+
577
+ await (engine as any).executePrimitive(action, context, new Map())
578
+
579
+ expect(context.getOutput('resolved-tx.hash')).toBeDefined()
580
+ })
581
+
582
+ it('should handle transaction without value and data', async () => {
583
+ const action: Action = {
584
+ type: 'send-transaction',
585
+ name: 'minimal-tx',
586
+ arguments: {
587
+ to: TEST_ADDRESSES.RECIPIENT_1
588
+ }
589
+ }
590
+
591
+ await (engine as any).executePrimitive(action, context, new Map())
592
+
593
+ expect(context.getOutput('minimal-tx.hash')).toBeDefined()
594
+ })
595
+
596
+ it('should not store outputs when action has no name', async () => {
597
+ const action: Action = {
598
+ type: 'send-transaction',
599
+ arguments: {
600
+ to: TEST_ADDRESSES.RECIPIENT_1,
601
+ value: TEST_VALUES.ONE_ETH,
602
+ data: '0x'
603
+ }
604
+ }
605
+
606
+ await (engine as any).executePrimitive(action, context, new Map())
607
+
608
+ // Since no name, no outputs should be stored
609
+ const outputs = (context as any).outputs
610
+ expect(outputs.size).toBe(0)
611
+ })
612
+
613
+ it('should throw on invalid address', async () => {
614
+ const action: Action = {
615
+ type: 'send-transaction',
616
+ arguments: {
617
+ to: 'invalid-address',
618
+ value: '1000000000000000000',
619
+ data: '0x'
620
+ }
621
+ }
622
+
623
+ await expect((engine as any).executePrimitive(action, context, new Map()))
624
+ .rejects.toThrow()
625
+ })
626
+
627
+ it('should apply gas multiplier when network gasLimit is set', async () => {
628
+ // Mock the network to have a gasLimit
629
+ const mockNetwork = { gasLimit: 100000 }
630
+ jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
631
+
632
+ const action: Action = {
633
+ type: 'send-transaction',
634
+ name: 'gas-multiplier-tx',
635
+ arguments: {
636
+ to: TEST_ADDRESSES.RECIPIENT_1,
637
+ value: TEST_VALUES.ONE_ETH,
638
+ gasMultiplier: 1.5
639
+ }
640
+ }
641
+
642
+ const mockSendTransaction = jest.fn().mockResolvedValue({
643
+ hash: '0x123',
644
+ wait: jest.fn().mockResolvedValue({ status: 1, blockNumber: 123 })
645
+ })
646
+ const resolvedSigner = await context.getResolvedSigner()
647
+ jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
648
+
649
+ await (engine as any).executePrimitive(action, context, new Map())
650
+
651
+ expect(mockSendTransaction).toHaveBeenCalledWith(
652
+ expect.objectContaining({
653
+ gasLimit: 150000 // 100000 * 1.5
654
+ })
655
+ )
656
+ })
657
+
658
+ it('should estimate gas and apply multiplier when no network gasLimit is set', async () => {
659
+ // Mock the network to have no gasLimit
660
+ const mockNetwork = {}
661
+ jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
662
+
663
+ const resolvedSigner = await context.getResolvedSigner()
664
+ const mockEstimateGas = jest.fn().mockResolvedValue(BigInt(80000))
665
+ jest.spyOn(resolvedSigner, 'estimateGas').mockImplementation(mockEstimateGas)
666
+
667
+ const mockSendTransaction = jest.fn().mockResolvedValue({
668
+ hash: '0x123',
669
+ wait: jest.fn().mockResolvedValue({ status: 1, blockNumber: 123 })
670
+ })
671
+ jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
672
+
673
+ const action: Action = {
674
+ type: 'send-transaction',
675
+ name: 'gas-estimate-tx',
676
+ arguments: {
677
+ to: TEST_ADDRESSES.RECIPIENT_1,
678
+ gasMultiplier: 2.0
679
+ }
680
+ }
681
+
682
+ await (engine as any).executePrimitive(action, context, new Map())
683
+
684
+ expect(mockEstimateGas).toHaveBeenCalledWith(
685
+ expect.objectContaining({
686
+ to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
687
+ })
688
+ )
689
+ expect(mockSendTransaction).toHaveBeenCalledWith(
690
+ expect.objectContaining({
691
+ gasLimit: 160000 // 80000 * 2.0
692
+ })
693
+ )
694
+ })
695
+
696
+ it('should work with resolved gasMultiplier value', async () => {
697
+ context.setOutput('multiplier', 1.25)
698
+ const mockNetwork = { gasLimit: 100000 }
699
+ jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
700
+
701
+ const mockSendTransaction = jest.fn().mockResolvedValue({
702
+ hash: '0x123',
703
+ wait: jest.fn().mockResolvedValue({ status: 1, blockNumber: 123 })
704
+ })
705
+ const resolvedSigner = await context.getResolvedSigner()
706
+ jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
707
+
708
+ const action: Action = {
709
+ type: 'send-transaction',
710
+ name: 'resolved-multiplier-tx',
711
+ arguments: {
712
+ to: TEST_ADDRESSES.RECIPIENT_1,
713
+ gasMultiplier: '{{multiplier}}'
714
+ }
715
+ }
716
+
717
+ await (engine as any).executePrimitive(action, context, new Map())
718
+
719
+ expect(mockSendTransaction).toHaveBeenCalledWith(
720
+ expect.objectContaining({
721
+ gasLimit: 125000 // 100000 * 1.25
722
+ })
723
+ )
724
+ })
725
+
726
+ it('should throw error for invalid gasMultiplier', async () => {
727
+ const action: Action = {
728
+ type: 'send-transaction',
729
+ arguments: {
730
+ to: TEST_ADDRESSES.RECIPIENT_1,
731
+ gasMultiplier: -1.0
732
+ }
733
+ }
734
+
735
+ await expect((engine as any).executePrimitive(action, context, new Map()))
736
+ .rejects.toThrow('gasMultiplier must be a positive number')
737
+ })
738
+
739
+ it('should throw error for zero gasMultiplier', async () => {
740
+ const action: Action = {
741
+ type: 'send-transaction',
742
+ arguments: {
743
+ to: TEST_ADDRESSES.RECIPIENT_1,
744
+ gasMultiplier: 0
745
+ }
746
+ }
747
+
748
+ await expect((engine as any).executePrimitive(action, context, new Map()))
749
+ .rejects.toThrow('gasMultiplier must be a positive number')
750
+ })
751
+ })
752
+
753
+ describe('create-contract', () => {
754
+ it('should create a contract successfully', async () => {
755
+ const action: Action = {
756
+ type: 'create-contract',
757
+ name: 'test-contract',
758
+ arguments: {
759
+ data: TEST_BYTECODES.SIMPLE_CONTRACT
760
+ }
761
+ }
762
+
763
+ await (engine as any).executePrimitive(action, context, new Map())
764
+
765
+ const hash = context.getOutput('test-contract.hash')
766
+ const receipt = context.getOutput('test-contract.receipt')
767
+ const address = context.getOutput('test-contract.address')
768
+
769
+ expect(hash).toBeDefined()
770
+ expect(receipt).toBeDefined()
771
+ expect(address).toBeDefined()
772
+ expect(receipt.status).toBe(1)
773
+ expect(receipt.contractAddress).toBe(address)
774
+ })
775
+
776
+ it('should create contract with resolved arguments', async () => {
777
+ context.setOutput('contract_bytecode', TEST_BYTECODES.SIMPLE_CONTRACT)
778
+
779
+ const action: Action = {
780
+ type: 'create-contract',
781
+ name: 'resolved-contract',
782
+ arguments: {
783
+ data: '{{contract_bytecode}}'
784
+ // Removed value parameter to avoid payable constructor issues
785
+ }
786
+ }
787
+
788
+ await (engine as any).executePrimitive(action, context, new Map())
789
+
790
+ expect(context.getOutput('resolved-contract.address')).toBeDefined()
791
+ })
792
+
793
+ it('should apply gas multiplier when network gasLimit is set', async () => {
794
+ // Mock the network to have a gasLimit
795
+ const mockNetwork = { gasLimit: 200000 }
796
+ jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
797
+
798
+ const action: Action = {
799
+ type: 'create-contract',
800
+ name: 'gas-multiplier-contract',
801
+ arguments: {
802
+ data: TEST_BYTECODES.SIMPLE_CONTRACT,
803
+ gasMultiplier: 1.5
804
+ }
805
+ }
806
+
807
+ const mockSendTransaction = jest.fn().mockResolvedValue({
808
+ hash: '0x123',
809
+ wait: jest.fn().mockResolvedValue({
810
+ status: 1,
811
+ blockNumber: 123,
812
+ contractAddress: '0x5FbDB2315678afecb367f032d93F642f64180aa3'
813
+ })
814
+ })
815
+ const resolvedSigner = await context.getResolvedSigner()
816
+ jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
817
+
818
+ await (engine as any).executePrimitive(action, context, new Map())
819
+
820
+ expect(mockSendTransaction).toHaveBeenCalledWith(
821
+ expect.objectContaining({
822
+ to: null, // Contract creation
823
+ gasLimit: 300000 // 200000 * 1.5
824
+ })
825
+ )
826
+ })
827
+
828
+ it('should estimate gas and apply multiplier when no network gasLimit is set', async () => {
829
+ // Mock the network to have no gasLimit
830
+ const mockNetwork = {}
831
+ jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
832
+
833
+ const resolvedSigner = await context.getResolvedSigner()
834
+ const mockEstimateGas = jest.fn().mockResolvedValue(BigInt(150000))
835
+ jest.spyOn(resolvedSigner, 'estimateGas').mockImplementation(mockEstimateGas)
836
+
837
+ const mockSendTransaction = jest.fn().mockResolvedValue({
838
+ hash: '0x123',
839
+ wait: jest.fn().mockResolvedValue({
840
+ status: 1,
841
+ blockNumber: 123,
842
+ contractAddress: '0x5FbDB2315678afecb367f032d93F642f64180aa3'
843
+ })
844
+ })
845
+ jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
846
+
847
+ const action: Action = {
848
+ type: 'create-contract',
849
+ name: 'gas-estimate-contract',
850
+ arguments: {
851
+ data: TEST_BYTECODES.SIMPLE_CONTRACT,
852
+ gasMultiplier: 2.0
853
+ }
854
+ }
855
+
856
+ await (engine as any).executePrimitive(action, context, new Map())
857
+
858
+ expect(mockEstimateGas).toHaveBeenCalledWith(
859
+ expect.objectContaining({
860
+ to: null,
861
+ data: TEST_BYTECODES.SIMPLE_CONTRACT
862
+ })
863
+ )
864
+ expect(mockSendTransaction).toHaveBeenCalledWith(
865
+ expect.objectContaining({
866
+ gasLimit: 300000 // 150000 * 2.0
867
+ })
868
+ )
869
+ })
870
+
871
+ it('should work with resolved gasMultiplier value', async () => {
872
+ context.setOutput('multiplier', 1.25)
873
+ const mockNetwork = { gasLimit: 200000 }
874
+ jest.spyOn(context, 'getNetwork').mockReturnValue(mockNetwork as any)
875
+
876
+ const mockSendTransaction = jest.fn().mockResolvedValue({
877
+ hash: '0x123',
878
+ wait: jest.fn().mockResolvedValue({
879
+ status: 1,
880
+ blockNumber: 123,
881
+ contractAddress: '0x5FbDB2315678afecb367f032d93F642f64180aa3'
882
+ })
883
+ })
884
+ const resolvedSigner = await context.getResolvedSigner()
885
+ jest.spyOn(resolvedSigner, 'sendTransaction').mockImplementation(mockSendTransaction)
886
+
887
+ const action: Action = {
888
+ type: 'create-contract',
889
+ name: 'resolved-multiplier-contract',
890
+ arguments: {
891
+ data: TEST_BYTECODES.SIMPLE_CONTRACT,
892
+ gasMultiplier: '{{multiplier}}'
893
+ }
894
+ }
895
+
896
+ await (engine as any).executePrimitive(action, context, new Map())
897
+
898
+ expect(mockSendTransaction).toHaveBeenCalledWith(
899
+ expect.objectContaining({
900
+ gasLimit: 250000 // 200000 * 1.25
901
+ })
902
+ )
903
+ })
904
+
905
+ it('should throw error for invalid gasMultiplier', async () => {
906
+ const action: Action = {
907
+ type: 'create-contract',
908
+ arguments: {
909
+ data: TEST_BYTECODES.SIMPLE_CONTRACT,
910
+ gasMultiplier: -1.0
911
+ }
912
+ }
913
+
914
+ await expect((engine as any).executePrimitive(action, context, new Map()))
915
+ .rejects.toThrow('gasMultiplier must be a positive number')
916
+ })
917
+
918
+ it('should throw error for zero gasMultiplier', async () => {
919
+ const action: Action = {
920
+ type: 'create-contract',
921
+ arguments: {
922
+ data: TEST_BYTECODES.SIMPLE_CONTRACT,
923
+ gasMultiplier: 0
924
+ }
925
+ }
926
+
927
+ await expect((engine as any).executePrimitive(action, context, new Map()))
928
+ .rejects.toThrow('gasMultiplier must be a positive number')
929
+ })
930
+
931
+ it('should handle contract creation without value and gasMultiplier', async () => {
932
+ const action: Action = {
933
+ type: 'create-contract',
934
+ name: 'minimal-contract',
935
+ arguments: {
936
+ data: TEST_BYTECODES.SIMPLE_CONTRACT
937
+ }
938
+ }
939
+
940
+ await (engine as any).executePrimitive(action, context, new Map())
941
+
942
+ expect(context.getOutput('minimal-contract.address')).toBeDefined()
943
+ })
944
+
945
+ it('should not store outputs when action has no name', async () => {
946
+ const action: Action = {
947
+ type: 'create-contract',
948
+ arguments: {
949
+ data: TEST_BYTECODES.SIMPLE_CONTRACT
950
+ }
951
+ }
952
+
953
+ const outputsBefore = (context as any).outputs.size
954
+
955
+ await (engine as any).executePrimitive(action, context, new Map())
956
+
957
+ // Since no name, no outputs should be stored
958
+ const outputsAfter = (context as any).outputs.size
959
+ expect(outputsAfter).toBe(outputsBefore)
960
+ })
961
+
962
+ it('should throw on invalid bytecode', async () => {
963
+ const action: Action = {
964
+ type: 'create-contract',
965
+ arguments: {
966
+ data: 'invalid-bytecode'
967
+ }
968
+ }
969
+
970
+ await expect((engine as any).executePrimitive(action, context, new Map()))
971
+ .rejects.toThrow()
972
+ })
973
+ })
974
+
975
+ describe('send-signed-transaction', () => {
976
+ it('should broadcast a signed transaction', async () => {
977
+ // Create a signed transaction using the same private key
978
+ const wallet = new ethers.Wallet(TEST_PRIVATE_KEY, anvilProvider)
979
+
980
+ const tx = await wallet.populateTransaction({
981
+ to: TEST_ADDRESSES.RECIPIENT_1,
982
+ value: ethers.parseEther('1'),
983
+ gasLimit: 21000
984
+ })
985
+
986
+ const signedTx = await wallet.signTransaction(tx)
987
+
988
+ const action: Action = {
989
+ type: 'send-signed-transaction',
990
+ name: 'signed-tx',
991
+ arguments: {
992
+ transaction: signedTx
993
+ }
994
+ }
995
+
996
+ await (engine as any).executePrimitive(action, context, new Map())
997
+
998
+ expect(context.getOutput('signed-tx.hash')).toBeDefined()
999
+ expect(context.getOutput('signed-tx.receipt')).toBeDefined()
1000
+ })
1001
+
1002
+ it('should resolve transaction from context', async () => {
1003
+ const wallet = new ethers.Wallet(TEST_PRIVATE_KEY, anvilProvider)
1004
+
1005
+ const tx = await wallet.populateTransaction({
1006
+ to: TEST_ADDRESSES.RECIPIENT_1,
1007
+ value: ethers.parseEther('1'),
1008
+ gasLimit: 21000
1009
+ })
1010
+
1011
+ const signedTx = await wallet.signTransaction(tx)
1012
+ context.setOutput('prepared_tx', signedTx)
1013
+
1014
+ const action: Action = {
1015
+ type: 'send-signed-transaction',
1016
+ name: 'resolved-signed-tx',
1017
+ arguments: {
1018
+ transaction: '{{prepared_tx}}'
1019
+ }
1020
+ }
1021
+
1022
+ await (engine as any).executePrimitive(action, context, new Map())
1023
+
1024
+ expect(context.getOutput('resolved-signed-tx.hash')).toBeDefined()
1025
+ })
1026
+ })
1027
+
1028
+ describe('static', () => {
1029
+ it('should return the provided value unchanged', async () => {
1030
+ const action: Action = {
1031
+ type: 'static',
1032
+ name: 'test-static',
1033
+ arguments: {
1034
+ value: 'hello world'
1035
+ }
1036
+ }
1037
+
1038
+ await (engine as any).executePrimitive(action, context, new Map())
1039
+
1040
+ expect(context.getOutput('test-static.value')).toBe('hello world')
1041
+ })
1042
+
1043
+ it('should resolve and return complex values', async () => {
1044
+ context.setOutput('input_value', { foo: 'bar', number: 42 })
1045
+
1046
+ const action: Action = {
1047
+ type: 'static',
1048
+ name: 'complex-static',
1049
+ arguments: {
1050
+ value: '{{input_value}}'
1051
+ }
1052
+ }
1053
+
1054
+ await (engine as any).executePrimitive(action, context, new Map())
1055
+
1056
+ expect(context.getOutput('complex-static.value')).toEqual({ foo: 'bar', number: 42 })
1057
+ })
1058
+
1059
+ it('should work with numeric values', async () => {
1060
+ const action: Action = {
1061
+ type: 'static',
1062
+ name: 'numeric-static',
1063
+ arguments: {
1064
+ value: 12345
1065
+ }
1066
+ }
1067
+
1068
+ await (engine as any).executePrimitive(action, context, new Map())
1069
+
1070
+ expect(context.getOutput('numeric-static.value')).toBe(12345)
1071
+ })
1072
+
1073
+ it('should work with boolean values', async () => {
1074
+ const action: Action = {
1075
+ type: 'static',
1076
+ name: 'boolean-static',
1077
+ arguments: {
1078
+ value: true
1079
+ }
1080
+ }
1081
+
1082
+ await (engine as any).executePrimitive(action, context, new Map())
1083
+
1084
+ expect(context.getOutput('boolean-static.value')).toBe(true)
1085
+ })
1086
+
1087
+ it('should work with array values', async () => {
1088
+ const testArray = [1, 2, 3, 'test']
1089
+ const action: Action = {
1090
+ type: 'static',
1091
+ name: 'array-static',
1092
+ arguments: {
1093
+ value: testArray
1094
+ }
1095
+ }
1096
+
1097
+ await (engine as any).executePrimitive(action, context, new Map())
1098
+
1099
+ expect(context.getOutput('array-static.value')).toEqual(testArray)
1100
+ })
1101
+
1102
+ it('should not store outputs when action has no name', async () => {
1103
+ const action: Action = {
1104
+ type: 'static',
1105
+ arguments: {
1106
+ value: 'test value'
1107
+ }
1108
+ }
1109
+
1110
+ const outputsBefore = Object.keys((context as any).outputs || {}).length
1111
+
1112
+ await (engine as any).executePrimitive(action, context, new Map())
1113
+
1114
+ const outputsAfter = Object.keys((context as any).outputs || {}).length
1115
+ expect(outputsAfter).toBe(outputsBefore)
1116
+ })
1117
+
1118
+ it('should resolve template variables in scope', async () => {
1119
+ const scope = new Map()
1120
+ scope.set('template_var', 'resolved from scope')
1121
+
1122
+ const action: Action = {
1123
+ type: 'static',
1124
+ name: 'scope-static',
1125
+ arguments: {
1126
+ value: '{{template_var}}'
1127
+ }
1128
+ }
1129
+
1130
+ await (engine as any).executePrimitive(action, context, scope)
1131
+
1132
+ expect(context.getOutput('scope-static.value')).toBe('resolved from scope')
1133
+ })
1134
+ })
1135
+
1136
+ it('should throw on unknown primitive action type', async () => {
1137
+ const action: any = {
1138
+ type: 'unknown-action',
1139
+ name: 'unknown',
1140
+ arguments: {}
1141
+ }
1142
+
1143
+ await expect((engine as any).executePrimitive(action, context, new Map()))
1144
+ .rejects.toThrow('Unknown or unimplemented primitive action type: unknown-action')
1145
+ })
1146
+
1147
+ it('should handle custom outputs for primitive actions', async () => {
1148
+ const action: JobAction = {
1149
+ name: 'custom-primitive',
1150
+ type: 'send-transaction',
1151
+ arguments: {
1152
+ to: TEST_ADDRESSES.RECIPIENT_1,
1153
+ value: TEST_VALUES.ONE_ETH,
1154
+ data: '0x'
1155
+ },
1156
+ output: {
1157
+ value: '0xDE280948Af8A9762B6984995C8c3c7F5AEB921Bf'
1158
+ } as any
1159
+ }
1160
+
1161
+ await (engine as any).executeAction(action, context, new Map())
1162
+
1163
+ // Should have the custom static output, not the default transaction outputs
1164
+ expect(context.getOutput('custom-primitive.value')).toBe('0xDE280948Af8A9762B6984995C8c3c7F5AEB921Bf')
1165
+
1166
+ // Default outputs should not be present when custom output is specified
1167
+ expect(() => context.getOutput('custom-primitive.hash')).toThrow()
1168
+ expect(() => context.getOutput('custom-primitive.receipt')).toThrow()
1169
+ })
1170
+ })
1171
+
1172
+ describe('evaluateSkipConditions', () => {
1173
+ it('should return false for undefined conditions', async () => {
1174
+ const result = await (engine as any).evaluateSkipConditions(undefined, context, new Map())
1175
+ expect(result).toBe(false)
1176
+ })
1177
+
1178
+ it('should return false for empty conditions array', async () => {
1179
+ const result = await (engine as any).evaluateSkipConditions([], context, new Map())
1180
+ expect(result).toBe(false)
1181
+ })
1182
+
1183
+ it('should return true if any condition is met', async () => {
1184
+ context.setOutput('flag1', 0)
1185
+ context.setOutput('flag2', 1)
1186
+
1187
+ const conditions = [
1188
+ { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{flag1}}', 1] } },
1189
+ { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{flag2}}', 1] } }
1190
+ ]
1191
+
1192
+ const result = await (engine as any).evaluateSkipConditions(conditions, context, new Map())
1193
+ expect(result).toBe(true)
1194
+ })
1195
+
1196
+ it('should return false if no conditions are met', async () => {
1197
+ context.setOutput('flag1', 0)
1198
+ context.setOutput('flag2', 0)
1199
+
1200
+ const conditions = [
1201
+ { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{flag1}}', 1] } },
1202
+ { type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{flag2}}', 1] } }
1203
+ ]
1204
+
1205
+ const result = await (engine as any).evaluateSkipConditions(conditions, context, new Map())
1206
+ expect(result).toBe(false)
1207
+ })
1208
+ })
1209
+
1210
+ describe('topologicalSortActions', () => {
1211
+ it('should sort actions with no dependencies', async () => {
1212
+ const job: Job = {
1213
+ name: 'no-deps-job',
1214
+ version: '1.0.0',
1215
+ actions: [
1216
+ { name: 'action-c', template: 'send-transaction', arguments: {} },
1217
+ { name: 'action-a', template: 'send-transaction', arguments: {} },
1218
+ { name: 'action-b', template: 'send-transaction', arguments: {} }
1219
+ ]
1220
+ }
1221
+
1222
+ const result = (engine as any).topologicalSortActions(job)
1223
+ expect(result).toEqual(['action-c', 'action-a', 'action-b'])
1224
+ })
1225
+
1226
+ it('should sort actions with dependencies correctly', async () => {
1227
+ const job: Job = {
1228
+ name: 'deps-job',
1229
+ version: '1.0.0',
1230
+ actions: [
1231
+ { name: 'action-c', template: 'send-transaction', arguments: {}, depends_on: ['action-a', 'action-b'] },
1232
+ { name: 'action-b', template: 'send-transaction', arguments: {}, depends_on: ['action-a'] },
1233
+ { name: 'action-a', template: 'send-transaction', arguments: {} }
1234
+ ]
1235
+ }
1236
+
1237
+ const result = (engine as any).topologicalSortActions(job)
1238
+ expect(result).toEqual(['action-a', 'action-b', 'action-c'])
1239
+ })
1240
+
1241
+ it('should throw on circular dependencies', async () => {
1242
+ const job: Job = {
1243
+ name: 'circular-job',
1244
+ version: '1.0.0',
1245
+ actions: [
1246
+ { name: 'action-a', template: 'send-transaction', arguments: {}, depends_on: ['action-b'] },
1247
+ { name: 'action-b', template: 'send-transaction', arguments: {}, depends_on: ['action-a'] }
1248
+ ]
1249
+ }
1250
+
1251
+ expect(() => (engine as any).topologicalSortActions(job))
1252
+ .toThrow('Circular dependency detected among actions in job "circular-job".')
1253
+ })
1254
+
1255
+ it('should throw on invalid dependency', async () => {
1256
+ const job: Job = {
1257
+ name: 'invalid-dep-job',
1258
+ version: '1.0.0',
1259
+ actions: [
1260
+ { name: 'action-a', template: 'send-transaction', arguments: {}, depends_on: ['non-existent'] }
1261
+ ]
1262
+ }
1263
+
1264
+ expect(() => (engine as any).topologicalSortActions(job))
1265
+ .toThrow('Action "action-a" in job "invalid-dep-job" has an invalid dependency on "non-existent", which does not exist.')
1266
+ })
1267
+ })
1268
+
1269
+ describe('integration tests', () => {
1270
+ it('should execute a complex job with templates, dependencies, and skip conditions', async () => {
1271
+ // Mock executeAction to track execution and avoid transaction issues
1272
+ const executedActions: string[] = []
1273
+ const originalExecuteAction = (engine as any).executeAction
1274
+ ;(engine as any).executeAction = async function(action: JobAction | Action, ctx: ExecutionContext, scope: any) {
1275
+ const actionName = 'name' in action ? action.name : action.type
1276
+ if (actionName) {
1277
+ executedActions.push(actionName)
1278
+ // Mock successful execution by setting outputs for template calls
1279
+ if (actionName === 'setup-step') {
1280
+ // Mock the template execution outputs and template outputs
1281
+ ctx.setOutput('fund-contract.hash', 'mock-fund-hash')
1282
+ ctx.setOutput('fund-contract.receipt', { status: 1, blockNumber: 200 })
1283
+ // Set template outputs (these are what the test expects)
1284
+ ctx.setOutput('setup-step.funded_address', '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC')
1285
+ ctx.setOutput('setup-step.fund_amount', '2000000000000000000')
1286
+ } else if (actionName === 'deploy-step') {
1287
+ ctx.setOutput('deploy-contract.hash', 'mock-deploy-hash')
1288
+ ctx.setOutput('deploy-contract.receipt', { status: 1, blockNumber: 201 })
1289
+ // Set template outputs
1290
+ ctx.setOutput('deploy-step.deployment_hash', 'mock-deploy-hash')
1291
+ } else if (actionName === 'final-check') {
1292
+ ctx.setOutput('final-check.hash', 'mock-final-hash')
1293
+ ctx.setOutput('final-check.receipt', { status: 1, blockNumber: 202 })
1294
+ }
1295
+ }
1296
+ }
1297
+
1298
+ // Create a template that sets up some state
1299
+ const setupTemplate: Template = {
1300
+ name: 'setup-template',
1301
+ actions: [
1302
+ {
1303
+ type: 'send-transaction',
1304
+ name: 'fund-contract',
1305
+ arguments: {
1306
+ to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
1307
+ value: '2000000000000000000',
1308
+ data: '0x'
1309
+ }
1310
+ }
1311
+ ],
1312
+ outputs: {
1313
+ funded_address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
1314
+ fund_amount: '2000000000000000000'
1315
+ }
1316
+ }
1317
+
1318
+ // Create a template that uses the setup output
1319
+ const deployTemplate: Template = {
1320
+ name: 'deploy-template',
1321
+ actions: [
1322
+ {
1323
+ type: 'send-transaction',
1324
+ name: 'deploy-contract',
1325
+ arguments: {
1326
+ to: '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65',
1327
+ value: '1000000000000000000',
1328
+ data: '0x608060405234801561000f575f5ffd5b50603e80601c5f395ff3fe60806040525f80fdfea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033'
1329
+ }
1330
+ }
1331
+ ],
1332
+ outputs: {
1333
+ deployment_hash: '{{deploy-contract.hash}}'
1334
+ }
1335
+ }
1336
+
1337
+ templates.set('setup-template', setupTemplate)
1338
+ templates.set('deploy-template', deployTemplate)
1339
+
1340
+ const complexJob: Job = {
1341
+ name: 'complex-job',
1342
+ version: '1.0.0',
1343
+ actions: [
1344
+ {
1345
+ name: 'deploy-step',
1346
+ template: 'deploy-template',
1347
+ arguments: {
1348
+ funded_address: '{{setup-step.funded_address}}',
1349
+ fund_amount: '{{setup-step.fund_amount}}'
1350
+ },
1351
+ depends_on: ['setup-step']
1352
+ },
1353
+ {
1354
+ name: 'setup-step',
1355
+ template: 'setup-template',
1356
+ arguments: {}
1357
+ },
1358
+ {
1359
+ name: 'final-check',
1360
+ template: 'send-transaction',
1361
+ arguments: {
1362
+ to: '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc',
1363
+ value: '1000000000000000000',
1364
+ data: '0x'
1365
+ },
1366
+ depends_on: ['deploy-step']
1367
+ }
1368
+ ]
1369
+ }
1370
+
1371
+ await engine.executeJob(complexJob, context)
1372
+
1373
+ // Restore original method
1374
+ ;(engine as any).executeAction = originalExecuteAction
1375
+
1376
+ // Verify execution order respects dependencies
1377
+ expect(executedActions).toEqual(['setup-step', 'deploy-step', 'final-check'])
1378
+
1379
+ // Verify all the expected outputs exist
1380
+ expect(context.getOutput('setup-step.funded_address')).toBe('0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC')
1381
+ expect(context.getOutput('setup-step.fund_amount')).toBe('2000000000000000000')
1382
+ expect(context.getOutput('deploy-step.deployment_hash')).toBe('mock-deploy-hash')
1383
+ expect(context.getOutput('final-check.hash')).toBe('mock-final-hash')
1384
+ })
1385
+
1386
+ it('should handle failed transactions appropriately', async () => {
1387
+ // Try to send to an invalid address (will cause transaction to fail at validation)
1388
+ const job: Job = {
1389
+ name: 'failing-job',
1390
+ version: '1.0.0',
1391
+ actions: [
1392
+ {
1393
+ name: 'failing-tx',
1394
+ template: 'send-transaction',
1395
+ arguments: {
1396
+ to: 'not-an-address',
1397
+ value: '1000000000000000000',
1398
+ data: '0x'
1399
+ }
1400
+ }
1401
+ ]
1402
+ }
1403
+
1404
+ await expect(engine.executeJob(job, context)).rejects.toThrow()
1405
+ })
1406
+ })
1407
+
1408
+ describe('setup skip conditions', () => {
1409
+ it('should skip setup actions when contract-exists condition is met', async () => {
1410
+ // Deploy a contract first using anvil_setCode
1411
+ const contractAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
1412
+ const miniContractBytecode = '0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80636df5b97a14610038578063f8a8fd6d14610068575b5f5ffd5b610052600480360381019061004d91906100da565b610086565b60405161005f9190610127565b60405180910390f35b61007061009b565b60405161007d9190610127565b60405180910390f35b5f8183610093919061016d565b905092915050565b5f602a905090565b5f5ffd5b5f819050919050565b6100b9816100a7565b81146100c3575f5ffd5b50565b5f813590506100d4816100b0565b92915050565b5f5f604083850312156100f0576100ef6100a3565b5b5f6100fd858286016100c6565b925050602061010e858286016100c6565b9150509250929050565b610121816100a7565b82525050565b5f60208201905061013a5f830184610118565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610177826100a7565b9150610182836100a7565b9250828202610190816100a7565b915082820484148315176101a7576101a6610140565b5b509291505056fea264697066735822122071d40daa3d2beacd91f29d29ccf1c0b6f312e805f50b37166267c0a2a55e6e6164736f6c634300081c0033'
1413
+ await anvilProvider.send('anvil_setCode', [contractAddress, miniContractBytecode])
1414
+
1415
+ // Create a template that should skip setup because the contract exists
1416
+ const template: Template = {
1417
+ name: 'test-contract-exists-skip',
1418
+ setup: {
1419
+ skip_condition: [
1420
+ { type: 'contract-exists', arguments: { address: contractAddress } }
1421
+ ],
1422
+ actions: [
1423
+ {
1424
+ type: 'send-transaction',
1425
+ name: 'setup-action',
1426
+ arguments: {
1427
+ to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
1428
+ value: '1000000000000000000',
1429
+ data: '0x'
1430
+ }
1431
+ }
1432
+ ]
1433
+ },
1434
+ actions: [
1435
+ {
1436
+ type: 'send-transaction',
1437
+ name: 'main-action',
1438
+ arguments: {
1439
+ to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
1440
+ value: '500000000000000000',
1441
+ data: '0x'
1442
+ }
1443
+ }
1444
+ ]
1445
+ }
1446
+
1447
+ templates.set('test-contract-exists-skip', template)
1448
+
1449
+ const action: JobAction = {
1450
+ name: 'test-skip',
1451
+ template: 'test-contract-exists-skip',
1452
+ arguments: {}
1453
+ }
1454
+
1455
+ await (engine as any).executeAction(action, context, new Map())
1456
+
1457
+ // Setup action should have been skipped, so no output
1458
+ expect(() => context.getOutput('setup-action.hash')).toThrow()
1459
+ // Main action should still have executed
1460
+ expect(context.getOutput('main-action.hash')).toBeDefined()
1461
+ })
1462
+ })
1463
+
1464
+ describe('test-nicks-method action', () => {
1465
+ it('should successfully test Nick\'s method with a simple contract', async () => {
1466
+ const action: Action = {
1467
+ type: 'test-nicks-method',
1468
+ name: 'nick-test',
1469
+ arguments: {
1470
+ bytecode: TEST_BYTECODES.SIMPLE_RETURN_42,
1471
+ gasPrice: ethers.parseUnits('10', 'gwei'),
1472
+ gasLimit: 100000n,
1473
+ fundingAmount: ethers.parseEther('0.001')
1474
+ }
1475
+ }
1476
+
1477
+ await (engine as any).executeAction(action, context, new Map())
1478
+
1479
+ // Should have success output
1480
+ expect(context.getOutput('nick-test.success')).toBe(true)
1481
+ })
1482
+
1483
+ it('should handle missing optional parameters', async () => {
1484
+ const action: Action = {
1485
+ type: 'test-nicks-method',
1486
+ name: 'nick-test-defaults',
1487
+ arguments: {
1488
+ bytecode: TEST_BYTECODES.SIMPLE_RETURN_42
1489
+ // All other parameters should use defaults
1490
+ }
1491
+ }
1492
+
1493
+ await (engine as any).executeAction(action, context, new Map())
1494
+
1495
+ // Should have success output
1496
+ expect(context.getOutput('nick-test-defaults.success')).toBe(true)
1497
+ })
1498
+
1499
+ it('should use default bytecode when none provided', async () => {
1500
+ const action: Action = {
1501
+ type: 'test-nicks-method',
1502
+ name: 'nick-test-default-bytecode',
1503
+ arguments: {
1504
+ // No bytecode provided - should use default
1505
+ }
1506
+ }
1507
+
1508
+ await (engine as any).executeAction(action, context, new Map())
1509
+
1510
+ // Should have success output
1511
+ expect(context.getOutput('nick-test-default-bytecode.success')).toBe(true)
1512
+ })
1513
+ })
1514
+ })