@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.
@@ -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
+ }