@nomicfoundation/edr 0.12.0-alpha.0 → 0.12.0-next.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/Cargo.toml +47 -16
- package/LICENSE +5 -1
- package/index.d.ts +773 -104
- package/index.js +31 -1
- package/package.json +20 -17
- package/src/account.rs +102 -55
- package/src/block.rs +2 -103
- package/src/call_override.rs +8 -8
- package/src/cast.rs +7 -29
- package/src/chains/generic.rs +1 -1
- package/src/chains/l1.rs +20 -18
- package/src/chains/op.rs +95 -38
- package/src/config.rs +305 -161
- package/src/context.rs +260 -21
- package/src/debug_trace.rs +2 -2
- package/src/instrument.rs +109 -0
- package/src/lib.rs +10 -0
- package/src/log.rs +12 -14
- package/src/logger.rs +28 -30
- package/src/mock.rs +68 -0
- package/src/precompile.rs +50 -0
- package/src/provider/response.rs +8 -5
- package/src/provider.rs +14 -8
- package/src/result.rs +18 -27
- package/src/scenarios.rs +2 -2
- package/src/serde.rs +57 -0
- package/src/solidity_tests/artifact.rs +184 -0
- package/src/solidity_tests/config.rs +725 -0
- package/src/solidity_tests/factory.rs +22 -0
- package/src/solidity_tests/l1.rs +68 -0
- package/src/solidity_tests/op.rs +69 -0
- package/src/solidity_tests/runner.rs +51 -0
- package/src/solidity_tests/test_results.rs +668 -0
- package/src/solidity_tests.rs +56 -0
- package/src/subscription.rs +1 -1
- package/src/trace/debug.rs +1 -1
- package/src/trace/exit.rs +4 -4
- package/src/trace/library_utils.rs +7 -2
- package/src/trace/return_data.rs +10 -10
- package/src/trace/solidity_stack_trace.rs +7 -5
- package/src/trace.rs +29 -38
- package/src/withdrawal.rs +3 -3
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
use std::{collections::HashMap, path::PathBuf};
|
|
2
|
+
|
|
3
|
+
use derive_more::Debug;
|
|
4
|
+
use edr_eth::hex;
|
|
5
|
+
use edr_solidity_tests::{
|
|
6
|
+
executors::invariant::InvariantConfig,
|
|
7
|
+
fuzz::FuzzConfig,
|
|
8
|
+
inspectors::cheatcodes::{CheatsConfigOptions, ExecutionContextConfig},
|
|
9
|
+
TestFilterConfig,
|
|
10
|
+
};
|
|
11
|
+
use foundry_cheatcodes::{FsPermissions, RpcEndpoint, RpcEndpoints};
|
|
12
|
+
use napi::{
|
|
13
|
+
bindgen_prelude::{BigInt, Uint8Array},
|
|
14
|
+
tokio::runtime,
|
|
15
|
+
Either, Status,
|
|
16
|
+
};
|
|
17
|
+
use napi_derive::napi;
|
|
18
|
+
|
|
19
|
+
use crate::{
|
|
20
|
+
account::AccountOverride,
|
|
21
|
+
cast::TryCast,
|
|
22
|
+
config::ObservabilityConfig,
|
|
23
|
+
serde::{
|
|
24
|
+
serialize_optional_bigint_as_struct, serialize_optional_uint8array_as_hex,
|
|
25
|
+
serialize_uint8array_as_hex,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/// Solidity test runner configuration arguments exposed through the ffi.
|
|
30
|
+
/// Docs based on <https://book.getfoundry.sh/reference/config/testing>.
|
|
31
|
+
#[napi(object)]
|
|
32
|
+
#[derive(Debug, serde::Serialize)]
|
|
33
|
+
pub struct SolidityTestRunnerConfigArgs {
|
|
34
|
+
/// The absolute path to the project root directory.
|
|
35
|
+
/// Relative paths in cheat codes are resolved against this path.
|
|
36
|
+
pub project_root: String,
|
|
37
|
+
/// Configures the permissions of cheat codes that access the file system.
|
|
38
|
+
pub fs_permissions: Option<Vec<PathPermission>>,
|
|
39
|
+
/// Whether to support the `testFail` prefix. Defaults to false.
|
|
40
|
+
pub test_fail: Option<bool>,
|
|
41
|
+
/// Address labels for traces. Defaults to none.
|
|
42
|
+
pub labels: Option<Vec<AddressLabel>>,
|
|
43
|
+
/// Whether to enable isolation of calls. In isolation mode all top-level
|
|
44
|
+
/// calls are executed as a separate transaction in a separate EVM
|
|
45
|
+
/// context, enabling more precise gas accounting and transaction state
|
|
46
|
+
/// changes.
|
|
47
|
+
/// Defaults to false.
|
|
48
|
+
pub isolate: Option<bool>,
|
|
49
|
+
/// Whether or not to enable the ffi cheatcode.
|
|
50
|
+
/// Warning: Enabling this cheatcode has security implications, as it allows
|
|
51
|
+
/// tests to execute arbitrary programs on your computer.
|
|
52
|
+
/// Defaults to false.
|
|
53
|
+
pub ffi: Option<bool>,
|
|
54
|
+
/// Allow expecting reverts with `expectRevert` at the same callstack depth
|
|
55
|
+
/// as the test. Defaults to false.
|
|
56
|
+
pub allow_internal_expect_revert: Option<bool>,
|
|
57
|
+
/// The value of `msg.sender` in tests as hex string.
|
|
58
|
+
/// Defaults to `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`.
|
|
59
|
+
#[debug("{:?}", sender.as_ref().map(hex::encode))]
|
|
60
|
+
#[serde(serialize_with = "serialize_optional_uint8array_as_hex")]
|
|
61
|
+
pub sender: Option<Uint8Array>,
|
|
62
|
+
/// The value of `tx.origin` in tests as hex string.
|
|
63
|
+
/// Defaults to `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`.
|
|
64
|
+
#[debug("{:?}", tx_origin.as_ref().map(hex::encode))]
|
|
65
|
+
#[serde(serialize_with = "serialize_optional_uint8array_as_hex")]
|
|
66
|
+
pub tx_origin: Option<Uint8Array>,
|
|
67
|
+
/// The initial balance of the sender in tests.
|
|
68
|
+
/// Defaults to `0xffffffffffffffffffffffff`.
|
|
69
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
70
|
+
pub initial_balance: Option<BigInt>,
|
|
71
|
+
/// The value of `block.number` in tests.
|
|
72
|
+
/// Defaults to `1`.
|
|
73
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
74
|
+
pub block_number: Option<BigInt>,
|
|
75
|
+
/// The value of the `chainid` opcode in tests.
|
|
76
|
+
/// Defaults to `31337`.
|
|
77
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
78
|
+
pub chain_id: Option<BigInt>,
|
|
79
|
+
/// The gas limit for each test case.
|
|
80
|
+
/// Defaults to `9_223_372_036_854_775_807` (`i64::MAX`).
|
|
81
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
82
|
+
pub gas_limit: Option<BigInt>,
|
|
83
|
+
/// The price of gas (in wei) in tests.
|
|
84
|
+
/// Defaults to `0`.
|
|
85
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
86
|
+
pub gas_price: Option<BigInt>,
|
|
87
|
+
/// The base fee per gas (in wei) in tests.
|
|
88
|
+
/// Defaults to `0`.
|
|
89
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
90
|
+
pub block_base_fee_per_gas: Option<BigInt>,
|
|
91
|
+
/// The value of `block.coinbase` in tests.
|
|
92
|
+
/// Defaults to `0x0000000000000000000000000000000000000000`.
|
|
93
|
+
#[serde(serialize_with = "serialize_optional_uint8array_as_hex")]
|
|
94
|
+
#[debug("{:?}", block_coinbase.as_ref().map(hex::encode))]
|
|
95
|
+
pub block_coinbase: Option<Uint8Array>,
|
|
96
|
+
/// The value of `block.timestamp` in tests.
|
|
97
|
+
/// Defaults to 1.
|
|
98
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
99
|
+
pub block_timestamp: Option<BigInt>,
|
|
100
|
+
/// The value of `block.difficulty` in tests.
|
|
101
|
+
/// Defaults to 0.
|
|
102
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
103
|
+
pub block_difficulty: Option<BigInt>,
|
|
104
|
+
/// The `block.gaslimit` value during EVM execution.
|
|
105
|
+
/// Defaults to none.
|
|
106
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
107
|
+
pub block_gas_limit: Option<BigInt>,
|
|
108
|
+
/// Whether to disable the block gas limit.
|
|
109
|
+
/// Defaults to false.
|
|
110
|
+
pub disable_block_gas_limit: Option<bool>,
|
|
111
|
+
/// The memory limit of the EVM in bytes.
|
|
112
|
+
/// Defaults to 33_554_432 (2^25 = 32MiB).
|
|
113
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
114
|
+
pub memory_limit: Option<BigInt>,
|
|
115
|
+
/// The predeploys applied in local mode. Defaults to no predeploys.
|
|
116
|
+
/// These should match the predeploys of the network in fork mode, so they
|
|
117
|
+
/// aren't set in fork mode.
|
|
118
|
+
/// The code must be set and non-empty. The nonce and the balance default to
|
|
119
|
+
/// zero and storage defaults to empty.
|
|
120
|
+
pub local_predeploys: Option<Vec<AccountOverride>>,
|
|
121
|
+
/// If set, all tests are run in fork mode using this url or remote name.
|
|
122
|
+
/// Defaults to none.
|
|
123
|
+
pub eth_rpc_url: Option<String>,
|
|
124
|
+
/// Pins the block number for the global state fork.
|
|
125
|
+
#[serde(serialize_with = "serialize_optional_bigint_as_struct")]
|
|
126
|
+
pub fork_block_number: Option<BigInt>,
|
|
127
|
+
/// Map of RPC endpoints from chain name to RPC urls for fork cheat codes,
|
|
128
|
+
/// e.g. `{ "optimism": "https://optimism.alchemyapi.io/v2/..." }`
|
|
129
|
+
pub rpc_endpoints: Option<HashMap<String, String>>,
|
|
130
|
+
/// Optional RPC cache path. If this is none, then no RPC calls will be
|
|
131
|
+
/// cached, otherwise data is cached to `<rpc_cache_path>/<chain
|
|
132
|
+
/// id>/<block number>`. Caching can be disabled for specific chains
|
|
133
|
+
/// with `rpc_storage_caching`.
|
|
134
|
+
pub rpc_cache_path: Option<String>,
|
|
135
|
+
/// What RPC endpoints are cached. Defaults to all.
|
|
136
|
+
pub rpc_storage_caching: Option<StorageCachingConfig>,
|
|
137
|
+
/// The number of seconds to wait before `vm.prompt` reverts with a timeout.
|
|
138
|
+
/// Defaults to 120.
|
|
139
|
+
pub prompt_timeout: Option<u32>,
|
|
140
|
+
/// Fuzz testing configuration.
|
|
141
|
+
pub fuzz: Option<FuzzConfigArgs>,
|
|
142
|
+
/// Invariant testing configuration.
|
|
143
|
+
/// If an invariant config setting is not set, but a corresponding fuzz
|
|
144
|
+
/// config value is set, then the fuzz config value will be used.
|
|
145
|
+
pub invariant: Option<InvariantConfigArgs>,
|
|
146
|
+
/// Controls which test results should include execution traces. Defaults to
|
|
147
|
+
/// None.
|
|
148
|
+
pub include_traces: Option<IncludeTraces>,
|
|
149
|
+
/// The configuration for the Solidity test runner's observability
|
|
150
|
+
#[debug(skip)]
|
|
151
|
+
#[serde(skip)]
|
|
152
|
+
pub observability: Option<ObservabilityConfig>,
|
|
153
|
+
/// A regex pattern to filter tests. If provided, only test methods that
|
|
154
|
+
/// match the pattern will be executed and reported as a test result.
|
|
155
|
+
pub test_pattern: Option<String>,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
impl SolidityTestRunnerConfigArgs {
|
|
159
|
+
/// Resolves the instance, converting it to a
|
|
160
|
+
/// [`edr_napi_core::solidity::config::TestRunnerConfig`].
|
|
161
|
+
pub fn resolve(
|
|
162
|
+
self,
|
|
163
|
+
env: &napi::Env,
|
|
164
|
+
runtime: runtime::Handle,
|
|
165
|
+
) -> napi::Result<edr_napi_core::solidity::config::TestRunnerConfig> {
|
|
166
|
+
let SolidityTestRunnerConfigArgs {
|
|
167
|
+
project_root,
|
|
168
|
+
fs_permissions,
|
|
169
|
+
test_fail,
|
|
170
|
+
labels,
|
|
171
|
+
isolate,
|
|
172
|
+
ffi,
|
|
173
|
+
allow_internal_expect_revert,
|
|
174
|
+
sender,
|
|
175
|
+
tx_origin,
|
|
176
|
+
initial_balance,
|
|
177
|
+
block_number,
|
|
178
|
+
chain_id,
|
|
179
|
+
gas_limit,
|
|
180
|
+
gas_price,
|
|
181
|
+
block_base_fee_per_gas,
|
|
182
|
+
block_coinbase,
|
|
183
|
+
block_timestamp,
|
|
184
|
+
block_difficulty,
|
|
185
|
+
block_gas_limit,
|
|
186
|
+
disable_block_gas_limit,
|
|
187
|
+
memory_limit,
|
|
188
|
+
local_predeploys,
|
|
189
|
+
eth_rpc_url,
|
|
190
|
+
rpc_cache_path,
|
|
191
|
+
fork_block_number,
|
|
192
|
+
rpc_endpoints,
|
|
193
|
+
rpc_storage_caching,
|
|
194
|
+
prompt_timeout,
|
|
195
|
+
fuzz,
|
|
196
|
+
invariant,
|
|
197
|
+
include_traces,
|
|
198
|
+
observability,
|
|
199
|
+
test_pattern,
|
|
200
|
+
} = self;
|
|
201
|
+
|
|
202
|
+
let test_pattern = TestFilterConfig {
|
|
203
|
+
test_pattern: test_pattern
|
|
204
|
+
.as_ref()
|
|
205
|
+
.map(|p| {
|
|
206
|
+
p.parse()
|
|
207
|
+
.map_err(|error| napi::Error::new(Status::InvalidArg, error))
|
|
208
|
+
})
|
|
209
|
+
.transpose()?,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
let local_predeploys = local_predeploys
|
|
213
|
+
.map(|local_predeploys| {
|
|
214
|
+
local_predeploys
|
|
215
|
+
.into_iter()
|
|
216
|
+
.map(TryInto::try_into)
|
|
217
|
+
.collect::<Result<Vec<_>, _>>()
|
|
218
|
+
})
|
|
219
|
+
.transpose()?;
|
|
220
|
+
|
|
221
|
+
let invariant: InvariantConfig = fuzz
|
|
222
|
+
.as_ref()
|
|
223
|
+
.map(|f| invariant.clone().unwrap_or_default().defaults_from_fuzz(f))
|
|
224
|
+
.or(invariant)
|
|
225
|
+
.map(TryFrom::try_from)
|
|
226
|
+
.transpose()?
|
|
227
|
+
.unwrap_or_default();
|
|
228
|
+
|
|
229
|
+
let fuzz: FuzzConfig = fuzz.map(TryFrom::try_from).transpose()?.unwrap_or_default();
|
|
230
|
+
|
|
231
|
+
let cheatcode = CheatsConfigOptions {
|
|
232
|
+
// TODO https://github.com/NomicFoundation/edr/issues/657
|
|
233
|
+
// If gas reporting or coverage is supported, take that into account here.
|
|
234
|
+
execution_context: ExecutionContextConfig::Test,
|
|
235
|
+
rpc_endpoints: rpc_endpoints
|
|
236
|
+
.map(|endpoints| {
|
|
237
|
+
RpcEndpoints::new(
|
|
238
|
+
endpoints
|
|
239
|
+
.into_iter()
|
|
240
|
+
.map(|(chain, url)| (chain, RpcEndpoint::Url(url))),
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
.unwrap_or_default(),
|
|
244
|
+
rpc_cache_path: rpc_cache_path.map(PathBuf::from),
|
|
245
|
+
rpc_storage_caching: rpc_storage_caching
|
|
246
|
+
.map(TryFrom::try_from)
|
|
247
|
+
.transpose()?
|
|
248
|
+
.unwrap_or_default(),
|
|
249
|
+
fs_permissions: FsPermissions::new(
|
|
250
|
+
fs_permissions
|
|
251
|
+
.unwrap_or_default()
|
|
252
|
+
.into_iter()
|
|
253
|
+
.map(Into::into),
|
|
254
|
+
),
|
|
255
|
+
prompt_timeout: prompt_timeout.map_or(120, Into::into),
|
|
256
|
+
labels: labels
|
|
257
|
+
.unwrap_or_default()
|
|
258
|
+
.into_iter()
|
|
259
|
+
.map(|AddressLabel { address, label }| Ok((address.try_cast()?, label)))
|
|
260
|
+
.collect::<Result<_, napi::Error>>()?,
|
|
261
|
+
allow_internal_expect_revert: allow_internal_expect_revert.unwrap_or(false),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
let on_collected_coverage_fn = observability.map_or_else(
|
|
265
|
+
|| Ok(None),
|
|
266
|
+
|observability| {
|
|
267
|
+
observability
|
|
268
|
+
.resolve(env, runtime)
|
|
269
|
+
.map(|config| config.on_collected_coverage_fn)
|
|
270
|
+
},
|
|
271
|
+
)?;
|
|
272
|
+
|
|
273
|
+
let config = edr_napi_core::solidity::config::TestRunnerConfig {
|
|
274
|
+
project_root: project_root.into(),
|
|
275
|
+
include_traces: include_traces.unwrap_or_default().into(),
|
|
276
|
+
test_fail: test_fail.unwrap_or_default(),
|
|
277
|
+
isolate,
|
|
278
|
+
ffi,
|
|
279
|
+
sender: sender.map(TryCast::try_cast).transpose()?,
|
|
280
|
+
tx_origin: tx_origin.map(TryCast::try_cast).transpose()?,
|
|
281
|
+
initial_balance: initial_balance.map(TryCast::try_cast).transpose()?,
|
|
282
|
+
block_number: block_number.map(TryCast::try_cast).transpose()?,
|
|
283
|
+
chain_id: chain_id.map(TryCast::try_cast).transpose()?,
|
|
284
|
+
gas_limit: gas_limit.map(TryCast::try_cast).transpose()?,
|
|
285
|
+
gas_price: gas_price.map(TryCast::try_cast).transpose()?,
|
|
286
|
+
block_base_fee_per_gas: block_base_fee_per_gas.map(TryCast::try_cast).transpose()?,
|
|
287
|
+
block_coinbase: block_coinbase.map(TryCast::try_cast).transpose()?,
|
|
288
|
+
block_timestamp: block_timestamp.map(TryCast::try_cast).transpose()?,
|
|
289
|
+
block_difficulty: block_difficulty.map(TryCast::try_cast).transpose()?,
|
|
290
|
+
block_gas_limit: block_gas_limit.map(TryCast::try_cast).transpose()?,
|
|
291
|
+
disable_block_gas_limit,
|
|
292
|
+
memory_limit: memory_limit.map(TryCast::try_cast).transpose()?,
|
|
293
|
+
local_predeploys,
|
|
294
|
+
fork_url: eth_rpc_url,
|
|
295
|
+
fork_block_number: fork_block_number.map(TryCast::try_cast).transpose()?,
|
|
296
|
+
cheatcode,
|
|
297
|
+
fuzz,
|
|
298
|
+
invariant,
|
|
299
|
+
on_collected_coverage_fn,
|
|
300
|
+
test_pattern,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
Ok(config)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Fuzz testing configuration
|
|
308
|
+
#[napi(object)]
|
|
309
|
+
#[derive(Clone, Default, Debug, serde::Serialize)]
|
|
310
|
+
pub struct FuzzConfigArgs {
|
|
311
|
+
/// Path where fuzz failures are recorded and replayed if set.
|
|
312
|
+
pub failure_persist_dir: Option<String>,
|
|
313
|
+
/// Name of the file to record fuzz failures, defaults to `failures`.
|
|
314
|
+
pub failure_persist_file: Option<String>,
|
|
315
|
+
/// The amount of fuzz runs to perform for each fuzz test case. Higher
|
|
316
|
+
/// values gives more confidence in results at the cost of testing
|
|
317
|
+
/// speed.
|
|
318
|
+
/// Defaults to 256.
|
|
319
|
+
pub runs: Option<u32>,
|
|
320
|
+
/// The maximum number of combined inputs that may be rejected before the
|
|
321
|
+
/// test as a whole aborts. “Global” filters apply to the whole test
|
|
322
|
+
/// case. If the test case is rejected, the whole thing is regenerated.
|
|
323
|
+
/// Defaults to 65536.
|
|
324
|
+
pub max_test_rejects: Option<u32>,
|
|
325
|
+
/// Hexadecimal string.
|
|
326
|
+
/// Optional seed for the fuzzing RNG algorithm.
|
|
327
|
+
/// Defaults to None.
|
|
328
|
+
pub seed: Option<String>,
|
|
329
|
+
/// Integer between 0 and 100.
|
|
330
|
+
/// The weight of the dictionary. A higher dictionary weight will bias the
|
|
331
|
+
/// fuzz inputs towards “interesting” values, e.g. boundary values like
|
|
332
|
+
/// type(uint256).max or contract addresses from your environment.
|
|
333
|
+
/// Defaults to 40.
|
|
334
|
+
pub dictionary_weight: Option<u32>,
|
|
335
|
+
/// The flag indicating whether to include values from storage.
|
|
336
|
+
/// Defaults to true.
|
|
337
|
+
pub include_storage: Option<bool>,
|
|
338
|
+
/// The flag indicating whether to include push bytes values.
|
|
339
|
+
/// Defaults to true.
|
|
340
|
+
pub include_push_bytes: Option<bool>,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
impl TryFrom<FuzzConfigArgs> for FuzzConfig {
|
|
344
|
+
type Error = napi::Error;
|
|
345
|
+
|
|
346
|
+
fn try_from(value: FuzzConfigArgs) -> Result<Self, Self::Error> {
|
|
347
|
+
let FuzzConfigArgs {
|
|
348
|
+
failure_persist_dir,
|
|
349
|
+
failure_persist_file,
|
|
350
|
+
runs,
|
|
351
|
+
max_test_rejects,
|
|
352
|
+
seed,
|
|
353
|
+
dictionary_weight,
|
|
354
|
+
include_storage,
|
|
355
|
+
include_push_bytes,
|
|
356
|
+
} = value;
|
|
357
|
+
|
|
358
|
+
let failure_persist_dir = failure_persist_dir.map(PathBuf::from);
|
|
359
|
+
let failure_persist_file = failure_persist_file.unwrap_or("failures".to_string());
|
|
360
|
+
let seed = seed
|
|
361
|
+
.map(|s| {
|
|
362
|
+
s.parse().map_err(|_err| {
|
|
363
|
+
napi::Error::new(Status::InvalidArg, format!("Invalid seed value: {s}"))
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
.transpose()?;
|
|
367
|
+
|
|
368
|
+
let mut fuzz = FuzzConfig {
|
|
369
|
+
seed,
|
|
370
|
+
failure_persist_dir,
|
|
371
|
+
failure_persist_file,
|
|
372
|
+
// TODO https://github.com/NomicFoundation/edr/issues/657
|
|
373
|
+
gas_report_samples: 0,
|
|
374
|
+
..FuzzConfig::default()
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
if let Some(runs) = runs {
|
|
378
|
+
fuzz.runs = runs;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if let Some(max_test_rejects) = max_test_rejects {
|
|
382
|
+
fuzz.max_test_rejects = max_test_rejects;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if let Some(dictionary_weight) = dictionary_weight {
|
|
386
|
+
fuzz.dictionary.dictionary_weight = dictionary_weight;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if let Some(include_storage) = include_storage {
|
|
390
|
+
fuzz.dictionary.include_storage = include_storage;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if let Some(include_push_bytes) = include_push_bytes {
|
|
394
|
+
fuzz.dictionary.include_push_bytes = include_push_bytes;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
Ok(fuzz)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
impl SolidityTestRunnerConfigArgs {
|
|
402
|
+
pub fn try_get_test_filter(&self) -> napi::Result<TestFilterConfig> {
|
|
403
|
+
let test_pattern = self
|
|
404
|
+
.test_pattern
|
|
405
|
+
.as_ref()
|
|
406
|
+
.map(|p| {
|
|
407
|
+
p.parse()
|
|
408
|
+
.map_err(|e| napi::Error::new(Status::InvalidArg, e))
|
|
409
|
+
})
|
|
410
|
+
.transpose()?;
|
|
411
|
+
Ok(TestFilterConfig { test_pattern })
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/// Invariant testing configuration.
|
|
416
|
+
#[napi(object)]
|
|
417
|
+
#[derive(Clone, Default, Debug, serde::Serialize)]
|
|
418
|
+
pub struct InvariantConfigArgs {
|
|
419
|
+
/// Path where invariant failures are recorded and replayed if set.
|
|
420
|
+
pub failure_persist_dir: Option<String>,
|
|
421
|
+
/// The number of runs that must execute for each invariant test group.
|
|
422
|
+
/// Defaults to 256.
|
|
423
|
+
pub runs: Option<u32>,
|
|
424
|
+
/// The number of calls executed to attempt to break invariants in one run.
|
|
425
|
+
/// Defaults to 500.
|
|
426
|
+
pub depth: Option<u32>,
|
|
427
|
+
/// Fails the invariant fuzzing if a revert occurs.
|
|
428
|
+
/// Defaults to false.
|
|
429
|
+
pub fail_on_revert: Option<bool>,
|
|
430
|
+
/// Overrides unsafe external calls when running invariant tests, useful for
|
|
431
|
+
/// e.g. performing reentrancy checks.
|
|
432
|
+
/// Defaults to false.
|
|
433
|
+
pub call_override: Option<bool>,
|
|
434
|
+
/// Integer between 0 and 100.
|
|
435
|
+
/// The weight of the dictionary. A higher dictionary weight will bias the
|
|
436
|
+
/// fuzz inputs towards “interesting” values, e.g. boundary values like
|
|
437
|
+
/// type(uint256).max or contract addresses from your environment.
|
|
438
|
+
/// Defaults to 40.
|
|
439
|
+
pub dictionary_weight: Option<u32>,
|
|
440
|
+
/// The flag indicating whether to include values from storage.
|
|
441
|
+
/// Defaults to true.
|
|
442
|
+
pub include_storage: Option<bool>,
|
|
443
|
+
/// The flag indicating whether to include push bytes values.
|
|
444
|
+
/// Defaults to true.
|
|
445
|
+
pub include_push_bytes: Option<bool>,
|
|
446
|
+
/// The maximum number of attempts to shrink a failed the sequence. Shrink
|
|
447
|
+
/// process is disabled if set to 0.
|
|
448
|
+
/// Defaults to 5000.
|
|
449
|
+
pub shrink_run_limit: Option<u32>,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
impl InvariantConfigArgs {
|
|
453
|
+
/// Fill in fields from the fuzz config if they are not set.
|
|
454
|
+
fn defaults_from_fuzz(mut self, fuzz: &FuzzConfigArgs) -> Self {
|
|
455
|
+
let FuzzConfigArgs {
|
|
456
|
+
failure_persist_dir,
|
|
457
|
+
runs,
|
|
458
|
+
dictionary_weight,
|
|
459
|
+
include_storage,
|
|
460
|
+
include_push_bytes,
|
|
461
|
+
// These aren't used in the invariant config.
|
|
462
|
+
failure_persist_file: _,
|
|
463
|
+
max_test_rejects: _,
|
|
464
|
+
seed: _,
|
|
465
|
+
} = fuzz;
|
|
466
|
+
|
|
467
|
+
if self.failure_persist_dir.is_none() {
|
|
468
|
+
self.failure_persist_dir.clone_from(failure_persist_dir);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if self.runs.is_none() {
|
|
472
|
+
self.runs = *runs;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if self.dictionary_weight.is_none() {
|
|
476
|
+
self.dictionary_weight = *dictionary_weight;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if self.include_storage.is_none() {
|
|
480
|
+
self.include_storage = *include_storage;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if self.include_push_bytes.is_none() {
|
|
484
|
+
self.include_push_bytes = *include_push_bytes;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
self
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
impl From<InvariantConfigArgs> for InvariantConfig {
|
|
492
|
+
fn from(value: InvariantConfigArgs) -> Self {
|
|
493
|
+
let InvariantConfigArgs {
|
|
494
|
+
failure_persist_dir,
|
|
495
|
+
runs,
|
|
496
|
+
depth,
|
|
497
|
+
fail_on_revert,
|
|
498
|
+
call_override,
|
|
499
|
+
dictionary_weight,
|
|
500
|
+
include_storage,
|
|
501
|
+
include_push_bytes,
|
|
502
|
+
shrink_run_limit,
|
|
503
|
+
} = value;
|
|
504
|
+
|
|
505
|
+
let failure_persist_dir = failure_persist_dir.map(PathBuf::from);
|
|
506
|
+
|
|
507
|
+
let mut invariant = InvariantConfig {
|
|
508
|
+
failure_persist_dir,
|
|
509
|
+
// TODO https://github.com/NomicFoundation/edr/issues/657
|
|
510
|
+
gas_report_samples: 0,
|
|
511
|
+
..InvariantConfig::default()
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
if let Some(runs) = runs {
|
|
515
|
+
invariant.runs = runs;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if let Some(depth) = depth {
|
|
519
|
+
invariant.depth = depth;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if let Some(fail_on_revert) = fail_on_revert {
|
|
523
|
+
invariant.fail_on_revert = fail_on_revert;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if let Some(call_override) = call_override {
|
|
527
|
+
invariant.call_override = call_override;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if let Some(dictionary_weight) = dictionary_weight {
|
|
531
|
+
invariant.dictionary.dictionary_weight = dictionary_weight;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if let Some(include_storage) = include_storage {
|
|
535
|
+
invariant.dictionary.include_storage = include_storage;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if let Some(include_push_bytes) = include_push_bytes {
|
|
539
|
+
invariant.dictionary.include_push_bytes = include_push_bytes;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if let Some(shrink_run_limit) = shrink_run_limit {
|
|
543
|
+
invariant.shrink_run_limit = shrink_run_limit;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
invariant
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/// Settings to configure caching of remote RPC endpoints.
|
|
551
|
+
#[napi(object)]
|
|
552
|
+
#[derive(Clone, Debug, serde::Serialize)]
|
|
553
|
+
pub struct StorageCachingConfig {
|
|
554
|
+
/// Chains to cache. Either all or none or a list of chain names, e.g.
|
|
555
|
+
/// ["optimism", "mainnet"].
|
|
556
|
+
pub chains: Either<CachedChains, Vec<String>>,
|
|
557
|
+
/// Endpoints to cache. Either all or remote or a regex.
|
|
558
|
+
pub endpoints: Either<CachedEndpoints, String>,
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
impl Default for StorageCachingConfig {
|
|
562
|
+
fn default() -> Self {
|
|
563
|
+
Self {
|
|
564
|
+
chains: Either::A(CachedChains::default()),
|
|
565
|
+
endpoints: Either::A(CachedEndpoints::default()),
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
impl TryFrom<StorageCachingConfig> for foundry_cheatcodes::StorageCachingConfig {
|
|
571
|
+
type Error = napi::Error;
|
|
572
|
+
|
|
573
|
+
fn try_from(value: StorageCachingConfig) -> Result<Self, Self::Error> {
|
|
574
|
+
let chains = match value.chains {
|
|
575
|
+
Either::A(chains) => chains.into(),
|
|
576
|
+
Either::B(chains) => {
|
|
577
|
+
let chains = chains
|
|
578
|
+
.into_iter()
|
|
579
|
+
.map(|c| {
|
|
580
|
+
c.parse()
|
|
581
|
+
.map_err(|c| napi::Error::new(Status::InvalidArg, c))
|
|
582
|
+
})
|
|
583
|
+
.collect::<Result<_, _>>()?;
|
|
584
|
+
foundry_cheatcodes::CachedChains::Chains(chains)
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
let endpoints = match value.endpoints {
|
|
588
|
+
Either::A(endpoints) => endpoints.into(),
|
|
589
|
+
Either::B(regex) => {
|
|
590
|
+
let regex = regex.parse().map_err(|_err| {
|
|
591
|
+
napi::Error::new(Status::InvalidArg, format!("Invalid regex: {regex}"))
|
|
592
|
+
})?;
|
|
593
|
+
foundry_cheatcodes::CachedEndpoints::Pattern(regex)
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
Ok(Self { chains, endpoints })
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/// What chains to cache
|
|
601
|
+
#[napi]
|
|
602
|
+
#[derive(Debug, Default, serde::Serialize)]
|
|
603
|
+
pub enum CachedChains {
|
|
604
|
+
/// Cache all chains
|
|
605
|
+
#[default]
|
|
606
|
+
All,
|
|
607
|
+
/// Don't cache anything
|
|
608
|
+
None,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
impl From<CachedChains> for foundry_cheatcodes::CachedChains {
|
|
612
|
+
fn from(value: CachedChains) -> Self {
|
|
613
|
+
match value {
|
|
614
|
+
CachedChains::All => foundry_cheatcodes::CachedChains::All,
|
|
615
|
+
CachedChains::None => foundry_cheatcodes::CachedChains::None,
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/// What endpoints to enable caching for
|
|
621
|
+
#[napi]
|
|
622
|
+
#[derive(Debug, Default, serde::Serialize)]
|
|
623
|
+
pub enum CachedEndpoints {
|
|
624
|
+
/// Cache all endpoints
|
|
625
|
+
#[default]
|
|
626
|
+
All,
|
|
627
|
+
/// Only cache non-local host endpoints
|
|
628
|
+
Remote,
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
impl From<CachedEndpoints> for foundry_cheatcodes::CachedEndpoints {
|
|
632
|
+
fn from(value: CachedEndpoints) -> Self {
|
|
633
|
+
match value {
|
|
634
|
+
CachedEndpoints::All => foundry_cheatcodes::CachedEndpoints::All,
|
|
635
|
+
CachedEndpoints::Remote => foundry_cheatcodes::CachedEndpoints::Remote,
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/// Represents an access permission to a single path
|
|
641
|
+
#[napi(object)]
|
|
642
|
+
#[derive(Clone, Debug, serde::Serialize)]
|
|
643
|
+
pub struct PathPermission {
|
|
644
|
+
/// Permission level to access the `path`
|
|
645
|
+
pub access: FsAccessPermission,
|
|
646
|
+
/// The targeted path guarded by the permission
|
|
647
|
+
pub path: String,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
impl From<PathPermission> for foundry_cheatcodes::PathPermission {
|
|
651
|
+
fn from(value: PathPermission) -> Self {
|
|
652
|
+
let PathPermission { access, path } = value;
|
|
653
|
+
Self {
|
|
654
|
+
access: access.into(),
|
|
655
|
+
path: path.into(),
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/// Determines the status of file system access
|
|
661
|
+
#[napi]
|
|
662
|
+
#[derive(Debug, serde::Serialize)]
|
|
663
|
+
pub enum FsAccessPermission {
|
|
664
|
+
/// FS access is allowed with `read` + `write` permission
|
|
665
|
+
ReadWrite,
|
|
666
|
+
/// Only reading is allowed
|
|
667
|
+
Read,
|
|
668
|
+
/// Only writing is allowed
|
|
669
|
+
Write,
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
impl From<FsAccessPermission> for foundry_cheatcodes::FsAccessPermission {
|
|
673
|
+
fn from(value: FsAccessPermission) -> Self {
|
|
674
|
+
match value {
|
|
675
|
+
FsAccessPermission::ReadWrite => foundry_cheatcodes::FsAccessPermission::ReadWrite,
|
|
676
|
+
FsAccessPermission::Read => foundry_cheatcodes::FsAccessPermission::Read,
|
|
677
|
+
FsAccessPermission::Write => foundry_cheatcodes::FsAccessPermission::Write,
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
#[napi(object)]
|
|
683
|
+
#[derive(Clone, Debug, serde::Serialize)]
|
|
684
|
+
pub struct AddressLabel {
|
|
685
|
+
/// The address to label
|
|
686
|
+
#[serde(serialize_with = "serialize_uint8array_as_hex")]
|
|
687
|
+
#[debug("{}", hex::encode(address))]
|
|
688
|
+
pub address: Uint8Array,
|
|
689
|
+
/// The label to assign to the address
|
|
690
|
+
pub label: String,
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/// Configuration for [`SolidityTestRunnerConfigArgs::include_traces`] that
|
|
694
|
+
/// controls execution trace decoding and inclusion in test results.
|
|
695
|
+
#[napi]
|
|
696
|
+
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
|
|
697
|
+
pub enum IncludeTraces {
|
|
698
|
+
/// No traces will be included in any test result.
|
|
699
|
+
#[default]
|
|
700
|
+
None,
|
|
701
|
+
/// Traces will be included only on the results of failed tests.
|
|
702
|
+
Failing,
|
|
703
|
+
/// Traces will be included in all test results.
|
|
704
|
+
All,
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
impl From<IncludeTraces> for edr_solidity_tests::IncludeTraces {
|
|
708
|
+
fn from(value: IncludeTraces) -> Self {
|
|
709
|
+
match value {
|
|
710
|
+
IncludeTraces::None => edr_solidity_tests::IncludeTraces::None,
|
|
711
|
+
IncludeTraces::Failing => edr_solidity_tests::IncludeTraces::Failing,
|
|
712
|
+
IncludeTraces::All => edr_solidity_tests::IncludeTraces::All,
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
impl From<edr_solidity_tests::IncludeTraces> for IncludeTraces {
|
|
718
|
+
fn from(value: edr_solidity_tests::IncludeTraces) -> Self {
|
|
719
|
+
match value {
|
|
720
|
+
edr_solidity_tests::IncludeTraces::None => IncludeTraces::None,
|
|
721
|
+
edr_solidity_tests::IncludeTraces::Failing => IncludeTraces::Failing,
|
|
722
|
+
edr_solidity_tests::IncludeTraces::All => IncludeTraces::All,
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|