@0xsequence/catapult 1.3.16 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +250 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/index.d.ts +1 -0
  6. package/dist/commands/index.d.ts.map +1 -1
  7. package/dist/commands/index.js +1 -0
  8. package/dist/commands/index.js.map +1 -1
  9. package/dist/commands/list.d.ts.map +1 -1
  10. package/dist/commands/list.js +12 -0
  11. package/dist/commands/list.js.map +1 -1
  12. package/dist/commands/provenance.d.ts +3 -0
  13. package/dist/commands/provenance.d.ts.map +1 -0
  14. package/dist/commands/provenance.js +138 -0
  15. package/dist/commands/provenance.js.map +1 -0
  16. package/dist/commands/run.d.ts.map +1 -1
  17. package/dist/commands/run.js +7 -4
  18. package/dist/commands/run.js.map +1 -1
  19. package/dist/lib/__tests__/deployer.spec.js +118 -1
  20. package/dist/lib/__tests__/deployer.spec.js.map +1 -1
  21. package/dist/lib/__tests__/network-utils.spec.js +53 -8
  22. package/dist/lib/__tests__/network-utils.spec.js.map +1 -1
  23. package/dist/lib/__tests__/provenance.spec.d.ts +2 -0
  24. package/dist/lib/__tests__/provenance.spec.d.ts.map +1 -0
  25. package/dist/lib/__tests__/provenance.spec.js +205 -0
  26. package/dist/lib/__tests__/provenance.spec.js.map +1 -0
  27. package/dist/lib/contracts/__tests__/repository.spec.js +243 -0
  28. package/dist/lib/contracts/__tests__/repository.spec.js.map +1 -1
  29. package/dist/lib/contracts/repository.d.ts +9 -1
  30. package/dist/lib/contracts/repository.d.ts.map +1 -1
  31. package/dist/lib/contracts/repository.js +93 -7
  32. package/dist/lib/contracts/repository.js.map +1 -1
  33. package/dist/lib/core/__tests__/assert-action.spec.d.ts +2 -0
  34. package/dist/lib/core/__tests__/assert-action.spec.d.ts.map +1 -0
  35. package/dist/lib/core/__tests__/assert-action.spec.js +377 -0
  36. package/dist/lib/core/__tests__/assert-action.spec.js.map +1 -0
  37. package/dist/lib/core/__tests__/engine.spec.js +80 -0
  38. package/dist/lib/core/__tests__/engine.spec.js.map +1 -1
  39. package/dist/lib/core/__tests__/loader.spec.js +29 -0
  40. package/dist/lib/core/__tests__/loader.spec.js.map +1 -1
  41. package/dist/lib/core/__tests__/resolver.spec.js +383 -0
  42. package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
  43. package/dist/lib/core/engine.d.ts.map +1 -1
  44. package/dist/lib/core/engine.js +33 -0
  45. package/dist/lib/core/engine.js.map +1 -1
  46. package/dist/lib/core/loader.d.ts +1 -0
  47. package/dist/lib/core/loader.d.ts.map +1 -1
  48. package/dist/lib/core/loader.js +6 -1
  49. package/dist/lib/core/loader.js.map +1 -1
  50. package/dist/lib/core/resolver.d.ts +2 -0
  51. package/dist/lib/core/resolver.d.ts.map +1 -1
  52. package/dist/lib/core/resolver.js +89 -0
  53. package/dist/lib/core/resolver.js.map +1 -1
  54. package/dist/lib/deployer.d.ts.map +1 -1
  55. package/dist/lib/deployer.js +21 -4
  56. package/dist/lib/deployer.js.map +1 -1
  57. package/dist/lib/index.d.ts +1 -0
  58. package/dist/lib/index.d.ts.map +1 -1
  59. package/dist/lib/index.js +1 -0
  60. package/dist/lib/index.js.map +1 -1
  61. package/dist/lib/parsers/__tests__/job.spec.js +77 -0
  62. package/dist/lib/parsers/__tests__/job.spec.js.map +1 -1
  63. package/dist/lib/parsers/__tests__/source.spec.d.ts +2 -0
  64. package/dist/lib/parsers/__tests__/source.spec.d.ts.map +1 -0
  65. package/dist/lib/parsers/__tests__/source.spec.js +121 -0
  66. package/dist/lib/parsers/__tests__/source.spec.js.map +1 -0
  67. package/dist/lib/parsers/index.d.ts +1 -0
  68. package/dist/lib/parsers/index.d.ts.map +1 -1
  69. package/dist/lib/parsers/index.js +1 -0
  70. package/dist/lib/parsers/index.js.map +1 -1
  71. package/dist/lib/parsers/job.d.ts.map +1 -1
  72. package/dist/lib/parsers/job.js +11 -0
  73. package/dist/lib/parsers/job.js.map +1 -1
  74. package/dist/lib/parsers/source.d.ts +4 -0
  75. package/dist/lib/parsers/source.d.ts.map +1 -0
  76. package/dist/lib/parsers/source.js +107 -0
  77. package/dist/lib/parsers/source.js.map +1 -0
  78. package/dist/lib/provenance.d.ts +34 -0
  79. package/dist/lib/provenance.d.ts.map +1 -0
  80. package/dist/lib/provenance.js +645 -0
  81. package/dist/lib/provenance.js.map +1 -0
  82. package/dist/lib/types/actions.d.ts +18 -2
  83. package/dist/lib/types/actions.d.ts.map +1 -1
  84. package/dist/lib/types/actions.js +1 -0
  85. package/dist/lib/types/actions.js.map +1 -1
  86. package/dist/lib/types/contracts.d.ts +3 -0
  87. package/dist/lib/types/contracts.d.ts.map +1 -1
  88. package/dist/lib/types/definitions.d.ts +1 -0
  89. package/dist/lib/types/definitions.d.ts.map +1 -1
  90. package/dist/lib/types/index.d.ts +1 -0
  91. package/dist/lib/types/index.d.ts.map +1 -1
  92. package/dist/lib/types/index.js +1 -0
  93. package/dist/lib/types/index.js.map +1 -1
  94. package/dist/lib/types/source.d.ts +24 -0
  95. package/dist/lib/types/source.d.ts.map +1 -0
  96. package/dist/lib/types/source.js +3 -0
  97. package/dist/lib/types/source.js.map +1 -0
  98. package/dist/lib/types/values.d.ts +33 -1
  99. package/dist/lib/types/values.d.ts.map +1 -1
  100. package/package.json +1 -1
  101. package/src/cli.ts +3 -2
  102. package/src/commands/index.ts +2 -1
  103. package/src/commands/list.ts +14 -1
  104. package/src/commands/provenance.ts +120 -0
  105. package/src/commands/run.ts +11 -6
  106. package/src/lib/__tests__/deployer.spec.ts +177 -1
  107. package/src/lib/__tests__/network-utils.spec.ts +63 -14
  108. package/src/lib/__tests__/provenance.spec.ts +208 -0
  109. package/src/lib/contracts/__tests__/repository.spec.ts +270 -2
  110. package/src/lib/contracts/repository.ts +112 -14
  111. package/src/lib/core/__tests__/assert-action.spec.ts +474 -0
  112. package/src/lib/core/__tests__/engine.spec.ts +116 -0
  113. package/src/lib/core/__tests__/loader.spec.ts +34 -1
  114. package/src/lib/core/__tests__/resolver.spec.ts +444 -1
  115. package/src/lib/core/engine.ts +52 -0
  116. package/src/lib/core/loader.ts +8 -2
  117. package/src/lib/core/resolver.ts +116 -0
  118. package/src/lib/deployer.ts +28 -4
  119. package/src/lib/index.ts +4 -1
  120. package/src/lib/parsers/__tests__/job.spec.ts +81 -0
  121. package/src/lib/parsers/__tests__/source.spec.ts +134 -0
  122. package/src/lib/parsers/index.ts +1 -0
  123. package/src/lib/parsers/job.ts +14 -2
  124. package/src/lib/parsers/source.ts +129 -0
  125. package/src/lib/provenance.ts +785 -0
  126. package/src/lib/types/actions.ts +22 -1
  127. package/src/lib/types/contracts.ts +4 -1
  128. package/src/lib/types/definitions.ts +7 -0
  129. package/src/lib/types/index.ts +1 -0
  130. package/src/lib/types/source.ts +26 -0
  131. package/src/lib/types/values.ts +71 -0
@@ -1,7 +1,7 @@
1
1
  import { ethers } from 'ethers'
2
2
  import { ValueResolver } from '../resolver'
3
3
  import { ExecutionContext } from '../context'
4
- import { BasicArithmeticValue, Network, ReadBalanceValue, ComputeCreate2Value, ConstructorEncodeValue, AbiEncodeValue, AbiPackValue, CallValue, ContractExistsValue, ComputeCreateValue, SliceBytesValue } from '../../types'
4
+ import { BasicArithmeticValue, Network, ReadBalanceValue, GetStorageAtValue, ComputeSlotValue, ComputeCreate2Value, ConstructorEncodeValue, AbiEncodeValue, AbiPackValue, CallValue, ContractExistsValue, ComputeCreateValue, SliceBytesValue } from '../../types'
5
5
  import { ContractRepository } from '../../contracts/repository'
6
6
 
7
7
  describe('ValueResolver', () => {
@@ -2050,4 +2050,447 @@ describe('ValueResolver', () => {
2050
2050
  expect(result).toBe('0x010203')
2051
2051
  })
2052
2052
  })
2053
+
2054
+ describe('get-storage-at', () => {
2055
+ const testAddress = '0x1234567890123456789012345678901234567890'
2056
+ const testSlot = '0x0000000000000000000000000000000000000000000000000000000000000000'
2057
+ const testSlotNumber = 0
2058
+ const expectedStorageValue = '0x0000000000000000000000000000000000000000000000000000000000000001'
2059
+ let anvilProvider: ethers.JsonRpcProvider
2060
+
2061
+ beforeEach(async () => {
2062
+ anvilProvider = context.provider as ethers.JsonRpcProvider
2063
+ })
2064
+
2065
+ it('should read storage slot with hex string slot', async () => {
2066
+ // Set storage at a specific slot using anvil_setStorageAt
2067
+ await anvilProvider.send('anvil_setStorageAt', [testAddress, testSlot, expectedStorageValue])
2068
+
2069
+ const value: GetStorageAtValue = {
2070
+ type: 'get-storage-at',
2071
+ arguments: {
2072
+ address: testAddress,
2073
+ slot: testSlot
2074
+ },
2075
+ }
2076
+ const result = await resolver.resolve(value, context)
2077
+ expect(result).toBe(expectedStorageValue)
2078
+ })
2079
+
2080
+ it('should read storage slot with numeric slot', async () => {
2081
+ // Set storage at slot 0
2082
+ await anvilProvider.send('anvil_setStorageAt', [testAddress, testSlot, expectedStorageValue])
2083
+
2084
+ const value: GetStorageAtValue = {
2085
+ type: 'get-storage-at',
2086
+ arguments: {
2087
+ address: testAddress,
2088
+ slot: testSlotNumber
2089
+ },
2090
+ }
2091
+ const result = await resolver.resolve(value, context)
2092
+ expect(result).toBe(expectedStorageValue)
2093
+ })
2094
+
2095
+ it('should read storage slot with string number slot', async () => {
2096
+ // Set storage at slot 0
2097
+ await anvilProvider.send('anvil_setStorageAt', [testAddress, testSlot, expectedStorageValue])
2098
+
2099
+ const value: GetStorageAtValue = {
2100
+ type: 'get-storage-at',
2101
+ arguments: {
2102
+ address: testAddress,
2103
+ slot: '0'
2104
+ },
2105
+ }
2106
+ const result = await resolver.resolve(value, context)
2107
+ expect(result).toBe(expectedStorageValue)
2108
+ })
2109
+
2110
+ it('should read different storage slots', async () => {
2111
+ const slot1 = '0x0000000000000000000000000000000000000000000000000000000000000000'
2112
+ const slot2 = '0x0000000000000000000000000000000000000000000000000000000000000001'
2113
+ const value1 = '0x00000000000000000000000000000000000000000000000000000000000000aa'
2114
+ const value2 = '0x00000000000000000000000000000000000000000000000000000000000000bb'
2115
+
2116
+ await anvilProvider.send('anvil_setStorageAt', [testAddress, slot1, value1])
2117
+ await anvilProvider.send('anvil_setStorageAt', [testAddress, slot2, value2])
2118
+
2119
+ const valueForSlot1: GetStorageAtValue = {
2120
+ type: 'get-storage-at',
2121
+ arguments: {
2122
+ address: testAddress,
2123
+ slot: slot1
2124
+ },
2125
+ }
2126
+ const valueForSlot2: GetStorageAtValue = {
2127
+ type: 'get-storage-at',
2128
+ arguments: {
2129
+ address: testAddress,
2130
+ slot: slot2
2131
+ },
2132
+ }
2133
+
2134
+ const result1 = await resolver.resolve(valueForSlot1, context)
2135
+ const result2 = await resolver.resolve(valueForSlot2, context)
2136
+
2137
+ expect(result1).toBe(value1)
2138
+ expect(result2).toBe(value2)
2139
+ })
2140
+
2141
+ it('should resolve address from context variable', async () => {
2142
+ context.setOutput('myAddress', testAddress)
2143
+ await anvilProvider.send('anvil_setStorageAt', [testAddress, testSlot, expectedStorageValue])
2144
+
2145
+ const value: GetStorageAtValue = {
2146
+ type: 'get-storage-at',
2147
+ arguments: {
2148
+ address: '{{myAddress}}',
2149
+ slot: testSlot
2150
+ },
2151
+ }
2152
+ const result = await resolver.resolve(value, context)
2153
+ expect(result).toBe(expectedStorageValue)
2154
+ })
2155
+
2156
+ it('should resolve slot from context variable', async () => {
2157
+ context.setOutput('mySlot', testSlot)
2158
+ await anvilProvider.send('anvil_setStorageAt', [testAddress, testSlot, expectedStorageValue])
2159
+
2160
+ const value: GetStorageAtValue = {
2161
+ type: 'get-storage-at',
2162
+ arguments: {
2163
+ address: testAddress,
2164
+ slot: '{{mySlot}}'
2165
+ },
2166
+ }
2167
+ const result = await resolver.resolve(value, context)
2168
+ expect(result).toBe(expectedStorageValue)
2169
+ })
2170
+
2171
+ it('should return zero storage for non-existent slot', async () => {
2172
+ const nonExistentSlot = '0x000000000000000000000000000000000000000000000000000000000000ffff'
2173
+
2174
+ const value: GetStorageAtValue = {
2175
+ type: 'get-storage-at',
2176
+ arguments: {
2177
+ address: testAddress,
2178
+ slot: nonExistentSlot
2179
+ },
2180
+ }
2181
+ const result = await resolver.resolve(value, context)
2182
+ // Non-existent slots return 0x00...00
2183
+ expect(result).toBe('0x0000000000000000000000000000000000000000000000000000000000000000')
2184
+ })
2185
+
2186
+ it('should throw error for invalid address', async () => {
2187
+ const value: GetStorageAtValue = {
2188
+ type: 'get-storage-at',
2189
+ arguments: {
2190
+ address: 'invalid-address',
2191
+ slot: testSlot
2192
+ },
2193
+ }
2194
+
2195
+ await expect(resolver.resolve(value, context)).rejects.toThrow('Invalid address: invalid-address')
2196
+ })
2197
+
2198
+ it('should throw error for null address', async () => {
2199
+ const value: GetStorageAtValue = {
2200
+ type: 'get-storage-at',
2201
+ arguments: {
2202
+ address: null as any,
2203
+ slot: testSlot
2204
+ },
2205
+ }
2206
+
2207
+ await expect(resolver.resolve(value, context)).rejects.toThrow('Invalid address: null')
2208
+ })
2209
+
2210
+ it('should throw error for undefined address', async () => {
2211
+ const value: GetStorageAtValue = {
2212
+ type: 'get-storage-at',
2213
+ arguments: {
2214
+ address: undefined as any,
2215
+ slot: testSlot
2216
+ },
2217
+ }
2218
+
2219
+ await expect(resolver.resolve(value, context)).rejects.toThrow('Invalid address: undefined')
2220
+ })
2221
+
2222
+ it('should handle checksummed addresses', async () => {
2223
+ const checksummedAddress = '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed'
2224
+ await anvilProvider.send('anvil_setStorageAt', [checksummedAddress, testSlot, expectedStorageValue])
2225
+
2226
+ const value: GetStorageAtValue = {
2227
+ type: 'get-storage-at',
2228
+ arguments: {
2229
+ address: checksummedAddress,
2230
+ slot: testSlot
2231
+ },
2232
+ }
2233
+ const result = await resolver.resolve(value, context)
2234
+ expect(result).toBe(expectedStorageValue)
2235
+ })
2236
+
2237
+ it('should handle lowercase addresses', async () => {
2238
+ const lowercaseAddress = testAddress.toLowerCase()
2239
+ await anvilProvider.send('anvil_setStorageAt', [lowercaseAddress, testSlot, expectedStorageValue])
2240
+
2241
+ const value: GetStorageAtValue = {
2242
+ type: 'get-storage-at',
2243
+ arguments: {
2244
+ address: lowercaseAddress,
2245
+ slot: testSlot
2246
+ },
2247
+ }
2248
+ const result = await resolver.resolve(value, context)
2249
+ expect(result).toBe(expectedStorageValue)
2250
+ })
2251
+
2252
+ it('should handle large slot numbers', async () => {
2253
+ // EIP-1967 implementation slot
2254
+ const eip1967Slot = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
2255
+ await anvilProvider.send('anvil_setStorageAt', [testAddress, eip1967Slot, expectedStorageValue])
2256
+
2257
+ const value: GetStorageAtValue = {
2258
+ type: 'get-storage-at',
2259
+ arguments: {
2260
+ address: testAddress,
2261
+ slot: eip1967Slot
2262
+ },
2263
+ }
2264
+ const result = await resolver.resolve(value, context)
2265
+ expect(result).toBe(expectedStorageValue)
2266
+ })
2267
+ })
2268
+
2269
+ describe('compute-slot', () => {
2270
+ describe('mapping', () => {
2271
+ it('should compute a mapping value slot with an address key', async () => {
2272
+ const value: ComputeSlotValue = {
2273
+ type: 'compute-slot',
2274
+ arguments: {
2275
+ kind: 'mapping',
2276
+ slot: 2,
2277
+ key: '0x1111111111111111111111111111111111111111',
2278
+ keyType: 'address',
2279
+ },
2280
+ }
2281
+ const result = await resolver.resolve(value, context)
2282
+ expect(result).toBe('0x06bb1b9bc4293ba066a12274418b7ea4df183c2e4e6b39591987369520ca3956')
2283
+ })
2284
+
2285
+ it('should default keyType to uint256', async () => {
2286
+ const value: ComputeSlotValue = {
2287
+ type: 'compute-slot',
2288
+ arguments: {
2289
+ kind: 'mapping',
2290
+ slot: 1,
2291
+ key: 5,
2292
+ },
2293
+ }
2294
+ const result = await resolver.resolve(value, context)
2295
+ expect(result).toBe('0xe2689cd4a84e23ad2f564004f1c9013e9589d260bde6380aba3ca7e09e4df40c')
2296
+ })
2297
+
2298
+ it('should pack dynamic (string) keys', async () => {
2299
+ const value: ComputeSlotValue = {
2300
+ type: 'compute-slot',
2301
+ arguments: {
2302
+ kind: 'mapping',
2303
+ slot: 7,
2304
+ key: 'hello',
2305
+ keyType: 'string',
2306
+ },
2307
+ }
2308
+ const result = await resolver.resolve(value, context)
2309
+ expect(result).toBe('0xa39e328cf6237afe41b514c6c18ccdc6b503f43ce841d4b5bca5e763723b44a9')
2310
+ })
2311
+
2312
+ it('should support nesting for nested mappings', async () => {
2313
+ // balances[a][b] where the outer mapping is at slot 0
2314
+ const value: ComputeSlotValue = {
2315
+ type: 'compute-slot',
2316
+ arguments: {
2317
+ kind: 'mapping',
2318
+ key: '0x2222222222222222222222222222222222222222',
2319
+ keyType: 'address',
2320
+ slot: {
2321
+ type: 'compute-slot',
2322
+ arguments: {
2323
+ kind: 'mapping',
2324
+ slot: 0,
2325
+ key: '0x1111111111111111111111111111111111111111',
2326
+ keyType: 'address',
2327
+ },
2328
+ },
2329
+ },
2330
+ }
2331
+ const result = await resolver.resolve(value, context)
2332
+ expect(result).toBe('0xd1360c905f62126f789549525bc3219e0fb5feba645fd0705768a3cc3bf3fc5c')
2333
+ })
2334
+
2335
+ it('should resolve references in arguments', async () => {
2336
+ context.setOutput('mapSlot', 2)
2337
+ context.setOutput('owner', '0x1111111111111111111111111111111111111111')
2338
+ const value: ComputeSlotValue = {
2339
+ type: 'compute-slot',
2340
+ arguments: {
2341
+ kind: 'mapping',
2342
+ slot: '{{mapSlot}}',
2343
+ key: '{{owner}}',
2344
+ keyType: 'address',
2345
+ },
2346
+ }
2347
+ const result = await resolver.resolve(value, context)
2348
+ expect(result).toBe('0x06bb1b9bc4293ba066a12274418b7ea4df183c2e4e6b39591987369520ca3956')
2349
+ })
2350
+
2351
+ it('should throw when key is missing', async () => {
2352
+ const value = {
2353
+ type: 'compute-slot',
2354
+ arguments: { kind: 'mapping', slot: 0 },
2355
+ } as unknown as ComputeSlotValue
2356
+ await expect(resolver.resolve(value, context)).rejects.toThrow('compute-slot (mapping): "key" is required')
2357
+ })
2358
+ })
2359
+
2360
+ describe('dynamic-array', () => {
2361
+ it('should compute an element slot', async () => {
2362
+ const value: ComputeSlotValue = {
2363
+ type: 'compute-slot',
2364
+ arguments: { kind: 'dynamic-array', slot: 3, index: 4 },
2365
+ }
2366
+ const result = await resolver.resolve(value, context)
2367
+ expect(result).toBe('0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85f')
2368
+ })
2369
+
2370
+ it('should default index to 0', async () => {
2371
+ const value: ComputeSlotValue = {
2372
+ type: 'compute-slot',
2373
+ arguments: { kind: 'dynamic-array', slot: 3 },
2374
+ }
2375
+ const result = await resolver.resolve(value, context)
2376
+ // keccak256(slot) with index 0
2377
+ expect(result).toBe(ethers.keccak256(ethers.toBeHex(3, 32)))
2378
+ })
2379
+
2380
+ it('should account for multi-slot elements via elementSize', async () => {
2381
+ const value: ComputeSlotValue = {
2382
+ type: 'compute-slot',
2383
+ arguments: { kind: 'dynamic-array', slot: 3, index: 1, elementSize: 2 },
2384
+ }
2385
+ const result = await resolver.resolve(value, context)
2386
+ expect(result).toBe('0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85d')
2387
+ })
2388
+ })
2389
+
2390
+ describe('struct-field', () => {
2391
+ it('should add the field offset to the base slot', async () => {
2392
+ const value: ComputeSlotValue = {
2393
+ type: 'compute-slot',
2394
+ arguments: { kind: 'struct-field', slot: 100, offset: 3 },
2395
+ }
2396
+ const result = await resolver.resolve(value, context)
2397
+ expect(result).toBe('0x0000000000000000000000000000000000000000000000000000000000000067')
2398
+ })
2399
+
2400
+ it('should throw when offset is missing', async () => {
2401
+ const value = {
2402
+ type: 'compute-slot',
2403
+ arguments: { kind: 'struct-field', slot: 1 },
2404
+ } as unknown as ComputeSlotValue
2405
+ await expect(resolver.resolve(value, context)).rejects.toThrow('compute-slot (struct-field): "offset" is required')
2406
+ })
2407
+ })
2408
+
2409
+ describe('erc7201', () => {
2410
+ it('should compute the namespaced root for the EIP-7201 example', async () => {
2411
+ const value: ComputeSlotValue = {
2412
+ type: 'compute-slot',
2413
+ arguments: { kind: 'erc7201', id: 'example.main' },
2414
+ }
2415
+ const result = await resolver.resolve(value, context)
2416
+ expect(result).toBe('0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500')
2417
+ })
2418
+
2419
+ it('should match OpenZeppelin Ownable namespace', async () => {
2420
+ const value: ComputeSlotValue = {
2421
+ type: 'compute-slot',
2422
+ arguments: { kind: 'erc7201', id: 'openzeppelin.storage.Ownable' },
2423
+ }
2424
+ const result = await resolver.resolve(value, context)
2425
+ expect(result).toBe('0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300')
2426
+ })
2427
+
2428
+ it('should throw on empty id', async () => {
2429
+ const value = {
2430
+ type: 'compute-slot',
2431
+ arguments: { kind: 'erc7201', id: '' },
2432
+ } as unknown as ComputeSlotValue
2433
+ await expect(resolver.resolve(value, context)).rejects.toThrow('compute-slot (erc7201): "id" must be a non-empty string')
2434
+ })
2435
+ })
2436
+
2437
+ describe('eip1967', () => {
2438
+ it('should compute the implementation slot', async () => {
2439
+ const value: ComputeSlotValue = {
2440
+ type: 'compute-slot',
2441
+ arguments: { kind: 'eip1967', name: 'implementation' },
2442
+ }
2443
+ const result = await resolver.resolve(value, context)
2444
+ expect(result).toBe('0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc')
2445
+ })
2446
+
2447
+ it('should compute the admin slot', async () => {
2448
+ const value: ComputeSlotValue = {
2449
+ type: 'compute-slot',
2450
+ arguments: { kind: 'eip1967', name: 'admin' },
2451
+ }
2452
+ const result = await resolver.resolve(value, context)
2453
+ expect(result).toBe('0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103')
2454
+ })
2455
+
2456
+ it('should compute the beacon slot', async () => {
2457
+ const value: ComputeSlotValue = {
2458
+ type: 'compute-slot',
2459
+ arguments: { kind: 'eip1967', name: 'beacon' },
2460
+ }
2461
+ const result = await resolver.resolve(value, context)
2462
+ expect(result).toBe('0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50')
2463
+ })
2464
+
2465
+ it('should throw on unknown name', async () => {
2466
+ const value = {
2467
+ type: 'compute-slot',
2468
+ arguments: { kind: 'eip1967', name: 'nope' },
2469
+ } as unknown as ComputeSlotValue
2470
+ await expect(resolver.resolve(value, context)).rejects.toThrow('compute-slot (eip1967): "name" must be one of')
2471
+ })
2472
+ })
2473
+
2474
+ it('should chain into get-storage-at as the slot argument', async () => {
2475
+ // The computed slot is a valid bytes32 hex that get-storage-at can consume.
2476
+ const computed = await resolver.resolve<string>({
2477
+ type: 'compute-slot',
2478
+ arguments: { kind: 'eip1967', name: 'implementation' },
2479
+ } as ComputeSlotValue, context)
2480
+ expect(computed).toBe('0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc')
2481
+ })
2482
+
2483
+ it('should throw when kind is missing', async () => {
2484
+ const value = { type: 'compute-slot', arguments: {} } as unknown as ComputeSlotValue
2485
+ await expect(resolver.resolve(value, context)).rejects.toThrow('compute-slot: "kind" is required')
2486
+ })
2487
+
2488
+ it('should throw on unknown kind', async () => {
2489
+ const value = {
2490
+ type: 'compute-slot',
2491
+ arguments: { kind: 'bogus' },
2492
+ } as unknown as ComputeSlotValue
2493
+ await expect(resolver.resolve(value, context)).rejects.toThrow('compute-slot: unknown kind "bogus"')
2494
+ })
2495
+ })
2053
2496
  })
@@ -987,6 +987,58 @@ export class ExecutionEngine {
987
987
  }
988
988
  break
989
989
  }
990
+ case 'assert': {
991
+ // Determine the source of ACTUAL value
992
+ let actual: any
993
+ let describe: string
994
+ if (action.arguments.to !== undefined) {
995
+ // Use eth_call via the call resolver
996
+ actual = await this.resolver.resolve(
997
+ { type: 'call', arguments: { to: action.arguments.to, signature: action.arguments.signature, values: action.arguments.values ?? [] } },
998
+ context,
999
+ scope,
1000
+ )
1001
+ describe = action.arguments.signature ?? 'call'
1002
+ } else {
1003
+ // Use the `actual` value resolver
1004
+ actual = await this.resolver.resolve(action.arguments.actual, context, scope)
1005
+ describe = action.arguments.signature ?? 'value'
1006
+ }
1007
+
1008
+ // Determine the comparator key and expected value (exactly one required)
1009
+ const comparatorKeys = ['eq', 'neq', 'gt', 'lt', 'gte', 'lte']
1010
+ const args = action.arguments as any
1011
+ const presentKeys = comparatorKeys.filter((key) => args[key] !== undefined)
1012
+ if (presentKeys.length === 0) {
1013
+ throw new Error(`Action "${actionName}": assert must have exactly one of: ${comparatorKeys.join(', ')}`)
1014
+ }
1015
+ if (presentKeys.length > 1) {
1016
+ throw new Error(`Action "${actionName}": assert must have exactly one comparator, but got: ${presentKeys.join(', ')}`)
1017
+ }
1018
+ const operation = presentKeys[0]
1019
+ let expected: any = args[operation]
1020
+
1021
+ // Resolve expected value
1022
+ expected = await this.resolver.resolve(expected, context, scope)
1023
+
1024
+ // Compare using basic-arithmetic (which returns a boolean for eq/neq/gt/lt/gte/lte)
1025
+ const ok = await this.resolver.resolve(
1026
+ { type: 'basic-arithmetic', arguments: { operation, values: [actual, expected] } },
1027
+ context,
1028
+ scope,
1029
+ )
1030
+
1031
+ if (!ok) {
1032
+ const messagePart = action.arguments.message ? `: ${action.arguments.message}` : ''
1033
+ throw new Error(`assert failed${messagePart}: ${describe} (actual=${actual}, expected=${expected}, op=${operation})`)
1034
+ }
1035
+
1036
+ // Store output if named and no custom output
1037
+ if (action.name && !hasCustomOutput) {
1038
+ context.setOutput(`${action.name}.actual`, actual)
1039
+ }
1040
+ break
1041
+ }
990
1042
  default:
991
1043
  throw new Error(`Unknown or unimplemented primitive action type: ${(action as any).type}`)
992
1044
  }
@@ -7,6 +7,7 @@ import { parseConstants } from '../parsers/constants'
7
7
 
8
8
  export interface ProjectLoaderOptions {
9
9
  loadStdTemplates?: boolean
10
+ loadContracts?: boolean
10
11
  }
11
12
 
12
13
  export class ProjectLoader {
@@ -28,7 +29,9 @@ export class ProjectLoader {
28
29
 
29
30
  async load() {
30
31
  // Load all contracts from the project root first
31
- await this.contractRepository.loadFrom(this.projectRoot)
32
+ if (this.options.loadContracts !== false) {
33
+ await this.contractRepository.loadFrom(this.projectRoot)
34
+ }
32
35
 
33
36
  // Load standard library templates (unless disabled)
34
37
  if (this.options.loadStdTemplates !== false) {
@@ -119,6 +122,9 @@ export class ProjectLoader {
119
122
  this.templates.set(template.name, template)
120
123
  continue
121
124
  }
125
+ if (raw && typeof raw === 'object' && (raw.type === 'source' || raw.type === 'constants')) {
126
+ continue
127
+ }
122
128
 
123
129
  const job = parseJob(content)
124
130
  job._path = filePath
@@ -244,4 +250,4 @@ export class ProjectLoader {
244
250
  return false
245
251
  }
246
252
  }
247
- }
253
+ }
@@ -8,6 +8,8 @@ import {
8
8
  ComputeCreateValue,
9
9
  ComputeCreate2Value,
10
10
  ReadBalanceValue,
11
+ GetStorageAtValue,
12
+ ComputeSlotValue,
11
13
  BasicArithmeticValue,
12
14
  CallValue,
13
15
  ContractExistsValue,
@@ -197,6 +199,10 @@ export class ValueResolver {
197
199
  return this.resolveComputeCreate2(resolvedArgs as ComputeCreate2Value['arguments'])
198
200
  case 'read-balance':
199
201
  return this.resolveReadBalance(resolvedArgs as ReadBalanceValue['arguments'], context)
202
+ case 'get-storage-at':
203
+ return this.resolveGetStorageAt(resolvedArgs as GetStorageAtValue['arguments'], context)
204
+ case 'compute-slot':
205
+ return this.resolveComputeSlot(resolvedArgs as ComputeSlotValue['arguments'])
200
206
  case 'basic-arithmetic':
201
207
  return this.resolveBasicArithmetic(resolvedArgs as BasicArithmeticValue['arguments'])
202
208
  case 'call':
@@ -375,6 +381,116 @@ export class ValueResolver {
375
381
  return balance.toString()
376
382
  }
377
383
 
384
+ private async resolveGetStorageAt(args: GetStorageAtValue['arguments'], context: ExecutionContext): Promise<string> {
385
+ const { address, slot } = args
386
+
387
+ // Check if the address is a valid address
388
+ if (!isAddress(address)) {
389
+ throw new Error(`Invalid address: ${address}`)
390
+ }
391
+
392
+ // Normalize the slot to a BigInt
393
+ // After resolution, slot should be a string or number
394
+ const slotValue = ethers.toBigInt(slot as string | number)
395
+
396
+ const storageValue = await context.provider.getStorage(address, slotValue)
397
+ // getStorage returns a hex string, ensure it's 32 bytes (64 hex chars + 0x)
398
+ return ethers.hexlify(storageValue)
399
+ }
400
+
401
+ /**
402
+ * Computes EVM storage slots for the common Solidity storage layouts.
403
+ * Always returns a 32-byte, 0x-prefixed lowercase hex string so the result can
404
+ * be fed straight into `get-storage-at` or nested as the `slot` of another
405
+ * `compute-slot` (e.g. nested mappings, structs inside mappings, ...).
406
+ */
407
+ private resolveComputeSlot(args: ComputeSlotValue['arguments']): string {
408
+ if (!args || typeof args !== 'object' || typeof (args as any).kind !== 'string') {
409
+ throw new Error('compute-slot: "kind" is required')
410
+ }
411
+
412
+ switch (args.kind) {
413
+ case 'mapping': {
414
+ const { slot, key, keyType } = args
415
+ if (slot === undefined || slot === null) {
416
+ throw new Error('compute-slot (mapping): "slot" is required')
417
+ }
418
+ if (key === undefined || key === null) {
419
+ throw new Error('compute-slot (mapping): "key" is required')
420
+ }
421
+ const baseSlot = ethers.toBigInt(slot as string | number)
422
+ const type = (keyType as string) || 'uint256'
423
+ try {
424
+ // mapping value slot = keccak256(h(key) . p)
425
+ // Dynamic key types (string/bytes) are packed; value types are ABI-encoded (left-padded to 32 bytes).
426
+ if (type === 'string' || type === 'bytes') {
427
+ return ethers.solidityPackedKeccak256([type, 'uint256'], [key, baseSlot])
428
+ }
429
+ const encoded = ethers.AbiCoder.defaultAbiCoder().encode([type, 'uint256'], [key, baseSlot])
430
+ return ethers.keccak256(encoded)
431
+ } catch (error) {
432
+ throw new Error(`compute-slot (mapping): failed to encode key as "${type}": ${error instanceof Error ? error.message : String(error)}`)
433
+ }
434
+ }
435
+
436
+ case 'dynamic-array': {
437
+ const { slot, index, elementSize } = args
438
+ if (slot === undefined || slot === null) {
439
+ throw new Error('compute-slot (dynamic-array): "slot" is required')
440
+ }
441
+ const baseSlot = ethers.toBigInt(slot as string | number)
442
+ const idx = index === undefined || index === null ? 0n : ethers.toBigInt(index as string | number)
443
+ const size = elementSize === undefined || elementSize === null ? 1n : ethers.toBigInt(elementSize as string | number)
444
+ // element slot = keccak256(slot) + index * elementSize
445
+ const dataStart = ethers.toBigInt(ethers.keccak256(ethers.toBeHex(baseSlot, 32)))
446
+ return ethers.toBeHex(dataStart + idx * size, 32)
447
+ }
448
+
449
+ case 'struct-field': {
450
+ const { slot, offset } = args
451
+ if (slot === undefined || slot === null) {
452
+ throw new Error('compute-slot (struct-field): "slot" is required')
453
+ }
454
+ if (offset === undefined || offset === null) {
455
+ throw new Error('compute-slot (struct-field): "offset" is required')
456
+ }
457
+ const result = ethers.toBigInt(slot as string | number) + ethers.toBigInt(offset as string | number)
458
+ return ethers.toBeHex(result, 32)
459
+ }
460
+
461
+ case 'erc7201': {
462
+ const { id } = args
463
+ if (typeof id !== 'string' || id.length === 0) {
464
+ throw new Error('compute-slot (erc7201): "id" must be a non-empty string')
465
+ }
466
+ // keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff))
467
+ const idHash = ethers.toBigInt(ethers.keccak256(ethers.toUtf8Bytes(id)))
468
+ const inner = ethers.keccak256(ethers.toBeHex(idHash - 1n, 32))
469
+ const mask = ((1n << 256n) - 1n) ^ 0xffn
470
+ return ethers.toBeHex(ethers.toBigInt(inner) & mask, 32)
471
+ }
472
+
473
+ case 'eip1967': {
474
+ const labels: Record<string, string> = {
475
+ implementation: 'eip1967.proxy.implementation',
476
+ admin: 'eip1967.proxy.admin',
477
+ beacon: 'eip1967.proxy.beacon',
478
+ }
479
+ const name = args.name as string
480
+ const label = labels[name]
481
+ if (!label) {
482
+ throw new Error(`compute-slot (eip1967): "name" must be one of implementation, admin, beacon (got "${name}")`)
483
+ }
484
+ // slot = keccak256("eip1967.proxy.<name>") - 1
485
+ const slot = ethers.toBigInt(ethers.keccak256(ethers.toUtf8Bytes(label))) - 1n
486
+ return ethers.toBeHex(slot, 32)
487
+ }
488
+
489
+ default:
490
+ throw new Error(`compute-slot: unknown kind "${(args as any).kind}"`)
491
+ }
492
+ }
493
+
378
494
  private resolveBasicArithmetic(args: BasicArithmeticValue['arguments']): string | boolean {
379
495
  if (!args.values || args.values.length < 2) {
380
496
  throw new Error(`basic-arithmetic requires at least 2 values, got ${args.values?.length ?? 0}`)