@0xsequence/catapult 1.3.17 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +12 -0
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/provenance.d.ts +3 -0
- package/dist/commands/provenance.d.ts.map +1 -0
- package/dist/commands/provenance.js +138 -0
- package/dist/commands/provenance.js.map +1 -0
- package/dist/lib/__tests__/deployer.spec.js +118 -1
- package/dist/lib/__tests__/deployer.spec.js.map +1 -1
- package/dist/lib/__tests__/provenance.spec.d.ts +2 -0
- package/dist/lib/__tests__/provenance.spec.d.ts.map +1 -0
- package/dist/lib/__tests__/provenance.spec.js +205 -0
- package/dist/lib/__tests__/provenance.spec.js.map +1 -0
- package/dist/lib/contracts/__tests__/repository.spec.js +243 -0
- package/dist/lib/contracts/__tests__/repository.spec.js.map +1 -1
- package/dist/lib/contracts/repository.d.ts +9 -1
- package/dist/lib/contracts/repository.d.ts.map +1 -1
- package/dist/lib/contracts/repository.js +93 -7
- package/dist/lib/contracts/repository.js.map +1 -1
- package/dist/lib/core/__tests__/assert-action.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/assert-action.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/assert-action.spec.js +377 -0
- package/dist/lib/core/__tests__/assert-action.spec.js.map +1 -0
- package/dist/lib/core/__tests__/engine.spec.js +80 -0
- package/dist/lib/core/__tests__/engine.spec.js.map +1 -1
- package/dist/lib/core/__tests__/loader.spec.js +29 -0
- package/dist/lib/core/__tests__/loader.spec.js.map +1 -1
- package/dist/lib/core/__tests__/resolver.spec.js +383 -0
- package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +33 -0
- package/dist/lib/core/engine.js.map +1 -1
- package/dist/lib/core/loader.d.ts +1 -0
- package/dist/lib/core/loader.d.ts.map +1 -1
- package/dist/lib/core/loader.js +6 -1
- package/dist/lib/core/loader.js.map +1 -1
- package/dist/lib/core/resolver.d.ts +2 -0
- package/dist/lib/core/resolver.d.ts.map +1 -1
- package/dist/lib/core/resolver.js +89 -0
- package/dist/lib/core/resolver.js.map +1 -1
- package/dist/lib/deployer.d.ts.map +1 -1
- package/dist/lib/deployer.js +21 -4
- package/dist/lib/deployer.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/parsers/__tests__/job.spec.js +77 -0
- package/dist/lib/parsers/__tests__/job.spec.js.map +1 -1
- package/dist/lib/parsers/__tests__/source.spec.d.ts +2 -0
- package/dist/lib/parsers/__tests__/source.spec.d.ts.map +1 -0
- package/dist/lib/parsers/__tests__/source.spec.js +121 -0
- package/dist/lib/parsers/__tests__/source.spec.js.map +1 -0
- package/dist/lib/parsers/index.d.ts +1 -0
- package/dist/lib/parsers/index.d.ts.map +1 -1
- package/dist/lib/parsers/index.js +1 -0
- package/dist/lib/parsers/index.js.map +1 -1
- package/dist/lib/parsers/job.d.ts.map +1 -1
- package/dist/lib/parsers/job.js +11 -0
- package/dist/lib/parsers/job.js.map +1 -1
- package/dist/lib/parsers/source.d.ts +4 -0
- package/dist/lib/parsers/source.d.ts.map +1 -0
- package/dist/lib/parsers/source.js +107 -0
- package/dist/lib/parsers/source.js.map +1 -0
- package/dist/lib/provenance.d.ts +34 -0
- package/dist/lib/provenance.d.ts.map +1 -0
- package/dist/lib/provenance.js +645 -0
- package/dist/lib/provenance.js.map +1 -0
- package/dist/lib/types/actions.d.ts +18 -2
- package/dist/lib/types/actions.d.ts.map +1 -1
- package/dist/lib/types/actions.js +1 -0
- package/dist/lib/types/actions.js.map +1 -1
- package/dist/lib/types/contracts.d.ts +3 -0
- package/dist/lib/types/contracts.d.ts.map +1 -1
- package/dist/lib/types/definitions.d.ts +1 -0
- package/dist/lib/types/definitions.d.ts.map +1 -1
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/source.d.ts +24 -0
- package/dist/lib/types/source.d.ts.map +1 -0
- package/dist/lib/types/source.js +3 -0
- package/dist/lib/types/source.js.map +1 -0
- package/dist/lib/types/values.d.ts +33 -1
- package/dist/lib/types/values.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -2
- package/src/commands/index.ts +2 -1
- package/src/commands/list.ts +14 -1
- package/src/commands/provenance.ts +120 -0
- package/src/lib/__tests__/deployer.spec.ts +177 -1
- package/src/lib/__tests__/provenance.spec.ts +208 -0
- package/src/lib/contracts/__tests__/repository.spec.ts +270 -2
- package/src/lib/contracts/repository.ts +112 -14
- package/src/lib/core/__tests__/assert-action.spec.ts +474 -0
- package/src/lib/core/__tests__/engine.spec.ts +116 -0
- package/src/lib/core/__tests__/loader.spec.ts +34 -1
- package/src/lib/core/__tests__/resolver.spec.ts +444 -1
- package/src/lib/core/engine.ts +52 -0
- package/src/lib/core/loader.ts +8 -2
- package/src/lib/core/resolver.ts +116 -0
- package/src/lib/deployer.ts +28 -4
- package/src/lib/index.ts +4 -1
- package/src/lib/parsers/__tests__/job.spec.ts +81 -0
- package/src/lib/parsers/__tests__/source.spec.ts +134 -0
- package/src/lib/parsers/index.ts +1 -0
- package/src/lib/parsers/job.ts +14 -2
- package/src/lib/parsers/source.ts +129 -0
- package/src/lib/provenance.ts +785 -0
- package/src/lib/types/actions.ts +22 -1
- package/src/lib/types/contracts.ts +4 -1
- package/src/lib/types/definitions.ts +7 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/types/source.ts +26 -0
- package/src/lib/types/values.ts +71 -0
|
@@ -1886,4 +1886,120 @@ describe('ExecutionEngine', () => {
|
|
|
1886
1886
|
)
|
|
1887
1887
|
})
|
|
1888
1888
|
})
|
|
1889
|
+
|
|
1890
|
+
describe('skip_if behavior', () => {
|
|
1891
|
+
it('should NOT post-check skip_if after job execution', async () => {
|
|
1892
|
+
// This is the key semantic difference: skip_if is ONLY a pre-skip gate
|
|
1893
|
+
// The pre-skip check for skip_if happens in the deployer, not in executeJob
|
|
1894
|
+
// So executeJob runs normally without any post-check for skip_if
|
|
1895
|
+
const jobWithSkipIf: Job = {
|
|
1896
|
+
name: 'skip-if-job',
|
|
1897
|
+
version: '1.0.0',
|
|
1898
|
+
skip_if: [
|
|
1899
|
+
{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }
|
|
1900
|
+
],
|
|
1901
|
+
actions: [
|
|
1902
|
+
{
|
|
1903
|
+
name: 'generate-payload',
|
|
1904
|
+
type: 'static',
|
|
1905
|
+
arguments: { value: 'payload-data' }
|
|
1906
|
+
}
|
|
1907
|
+
]
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// Set context so skip_if would evaluate to false if checked
|
|
1911
|
+
context.setOutput('should_skip', 0)
|
|
1912
|
+
|
|
1913
|
+
// This should succeed because skip_if is NOT post-checked in executeJob
|
|
1914
|
+
// The job has no skip_condition, so no post-check happens
|
|
1915
|
+
await expect(engine.executeJob(jobWithSkipIf, context)).resolves.not.toThrow()
|
|
1916
|
+
|
|
1917
|
+
// Action should have been executed
|
|
1918
|
+
expect(context.getOutput('generate-payload.value')).toBe('payload-data')
|
|
1919
|
+
})
|
|
1920
|
+
|
|
1921
|
+
it('should post-check skip_condition but NOT skip_if', async () => {
|
|
1922
|
+
// Job with skip_condition - should post-check and fail
|
|
1923
|
+
const jobWithSkipCondition: Job = {
|
|
1924
|
+
name: 'skip-condition-job',
|
|
1925
|
+
version: '1.0.0',
|
|
1926
|
+
skip_condition: [
|
|
1927
|
+
{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{converged}}', 1] } }
|
|
1928
|
+
],
|
|
1929
|
+
actions: [
|
|
1930
|
+
{
|
|
1931
|
+
name: 'do-work',
|
|
1932
|
+
type: 'static',
|
|
1933
|
+
arguments: { value: 'work-done' }
|
|
1934
|
+
}
|
|
1935
|
+
]
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// Pre-execution: skip_condition is false (0) -> job runs
|
|
1939
|
+
context.setOutput('converged', 0)
|
|
1940
|
+
|
|
1941
|
+
// Post-execution: skip_condition is STILL false -> should FAIL
|
|
1942
|
+
// The post-check re-evaluates skip_condition and throws if not true
|
|
1943
|
+
await expect(engine.executeJob(jobWithSkipCondition, context)).rejects.toThrow('failed post-execution check')
|
|
1944
|
+
})
|
|
1945
|
+
|
|
1946
|
+
it('should not post-check skip_if (only skip_condition is post-checked)', async () => {
|
|
1947
|
+
// Job with only skip_if - no post-check should happen
|
|
1948
|
+
const jobWithOnlySkipIf: Job = {
|
|
1949
|
+
name: 'skip-if-only-job',
|
|
1950
|
+
version: '1.0.0',
|
|
1951
|
+
skip_if: [
|
|
1952
|
+
{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }
|
|
1953
|
+
],
|
|
1954
|
+
actions: [
|
|
1955
|
+
{
|
|
1956
|
+
name: 'generate-artifact',
|
|
1957
|
+
type: 'static',
|
|
1958
|
+
arguments: { value: 'artifact-data' }
|
|
1959
|
+
}
|
|
1960
|
+
]
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Set context so skip_if would be true if checked post-execution
|
|
1964
|
+
context.setOutput('should_skip', 1)
|
|
1965
|
+
|
|
1966
|
+
// But since skip_if is NOT post-checked (only skip_condition is),
|
|
1967
|
+
// and the job has no skip_condition, no post-check happens
|
|
1968
|
+
await expect(engine.executeJob(jobWithOnlySkipIf, context)).resolves.not.toThrow()
|
|
1969
|
+
|
|
1970
|
+
// Action should have been executed
|
|
1971
|
+
expect(context.getOutput('generate-artifact.value')).toBe('artifact-data')
|
|
1972
|
+
})
|
|
1973
|
+
|
|
1974
|
+
it('should post-check skip_condition when both skip_if and skip_condition are present', async () => {
|
|
1975
|
+
// Job with BOTH skip_if and skip_condition
|
|
1976
|
+
// skip_if is only pre-checked (in deployer)
|
|
1977
|
+
// skip_condition is both pre-checked and post-checked
|
|
1978
|
+
const jobWithBoth: Job = {
|
|
1979
|
+
name: 'both-conditions-job',
|
|
1980
|
+
version: '1.0.0',
|
|
1981
|
+
skip_condition: [
|
|
1982
|
+
{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{converged}}', 1] } }
|
|
1983
|
+
],
|
|
1984
|
+
skip_if: [
|
|
1985
|
+
{ type: 'basic-arithmetic', arguments: { operation: 'eq', values: ['{{should_skip}}', 1] } }
|
|
1986
|
+
],
|
|
1987
|
+
actions: [
|
|
1988
|
+
{
|
|
1989
|
+
name: 'do-work',
|
|
1990
|
+
type: 'static',
|
|
1991
|
+
arguments: { value: 'work-done' }
|
|
1992
|
+
}
|
|
1993
|
+
]
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// Pre-execution: both conditions are false -> job runs
|
|
1997
|
+
context.setOutput('should_skip', 0)
|
|
1998
|
+
context.setOutput('converged', 0)
|
|
1999
|
+
|
|
2000
|
+
// Post-execution: skip_condition is STILL false -> should FAIL
|
|
2001
|
+
// skip_if is NOT post-checked
|
|
2002
|
+
await expect(engine.executeJob(jobWithBoth, context)).rejects.toThrow('failed post-execution check')
|
|
2003
|
+
})
|
|
2004
|
+
})
|
|
1889
2005
|
})
|
|
@@ -154,6 +154,39 @@ actions: []`
|
|
|
154
154
|
expect(loader.jobs.size).toBe(1)
|
|
155
155
|
expect(loader.jobs.has('valid-job')).toBe(true)
|
|
156
156
|
})
|
|
157
|
+
|
|
158
|
+
it('should skip known non-job YAML documents under jobs without warning', async () => {
|
|
159
|
+
const jobsDir = path.join(tempDir, 'jobs')
|
|
160
|
+
const buildInfoDir = path.join(jobsDir, 'stack', 'build-info', 'rc-5')
|
|
161
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
162
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
163
|
+
|
|
164
|
+
const validJobYaml = `name: "valid-job"
|
|
165
|
+
version: "1"
|
|
166
|
+
actions: []`
|
|
167
|
+
|
|
168
|
+
await fs.writeFile(path.join(jobsDir, 'valid-job.yaml'), validJobYaml)
|
|
169
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
170
|
+
type: source
|
|
171
|
+
build_info: {}
|
|
172
|
+
`)
|
|
173
|
+
await fs.writeFile(path.join(jobsDir, 'constants.yaml'), `
|
|
174
|
+
type: constants
|
|
175
|
+
constants:
|
|
176
|
+
value: 1
|
|
177
|
+
`)
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const loader = new ProjectLoader(tempDir)
|
|
181
|
+
await loader.load()
|
|
182
|
+
|
|
183
|
+
expect(loader.jobs.size).toBe(1)
|
|
184
|
+
expect(loader.jobs.has('valid-job')).toBe(true)
|
|
185
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
186
|
+
} finally {
|
|
187
|
+
warnSpy.mockRestore()
|
|
188
|
+
}
|
|
189
|
+
})
|
|
157
190
|
})
|
|
158
191
|
|
|
159
192
|
describe('template loading', () => {
|
|
@@ -331,4 +364,4 @@ actions:
|
|
|
331
364
|
expect(job.description).toBe('Deploy a test contract')
|
|
332
365
|
})
|
|
333
366
|
})
|
|
334
|
-
})
|
|
367
|
+
})
|
|
@@ -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
|
})
|
package/src/lib/core/engine.ts
CHANGED
|
@@ -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
|
}
|