@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/src/config.rs CHANGED
@@ -1,40 +1,66 @@
1
+ use core::fmt::{Debug, Display};
1
2
  use std::{
2
3
  num::NonZeroU64,
4
+ path::PathBuf,
3
5
  time::{Duration, SystemTime},
4
6
  };
5
7
 
6
- use edr_eth::KECCAK_EMPTY;
8
+ use edr_coverage::reporter::SyncOnCollectedCoverageCallback;
9
+ use edr_eth::{
10
+ signature::{secret_key_from_str, SecretKey},
11
+ Bytes, HashMap, HashSet,
12
+ };
7
13
  use napi::{
8
- Either,
9
- bindgen_prelude::{BigInt, Buffer, Uint8Array},
14
+ bindgen_prelude::{BigInt, Promise, Reference, Uint8Array},
15
+ threadsafe_function::{
16
+ ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,
17
+ },
18
+ tokio::runtime,
19
+ Either, JsFunction, JsString, JsStringUtf8,
10
20
  };
11
21
  use napi_derive::napi;
12
22
 
13
- use crate::{
14
- account::{Account, OwnedAccount, StorageSlot},
15
- block::BlobGas,
16
- cast::TryCast,
17
- };
23
+ use crate::{account::AccountOverride, block::BlobGas, cast::TryCast, precompile::Precompile};
18
24
 
19
- /// Configuration for a chain
25
+ /// Specification of a chain with possible overrides.
20
26
  #[napi(object)]
21
- pub struct ChainConfig {
27
+ pub struct ChainOverride {
22
28
  /// The chain ID
23
29
  pub chain_id: BigInt,
24
- /// The chain's supported hardforks
25
- pub hardforks: Vec<HardforkActivation>,
30
+ /// The chain's name
31
+ pub name: String,
32
+ /// If present, overrides for the chain's supported hardforks
33
+ pub hardfork_activation_overrides: Option<Vec<HardforkActivation>>,
34
+ }
35
+
36
+ /// Configuration for a code coverage reporter.
37
+ #[napi(object)]
38
+ pub struct CodeCoverageConfig {
39
+ /// The callback to be called when coverage has been collected.
40
+ ///
41
+ /// The callback receives an array of unique coverage hit markers (i.e. no
42
+ /// repetition) per transaction.
43
+ ///
44
+ /// Exceptions thrown in the callback will be propagated to the original
45
+ /// caller.
46
+ #[napi(ts_type = "(coverageHits: Uint8Array[]) => Promise<void>")]
47
+ pub on_collected_coverage_callback: JsFunction,
26
48
  }
27
49
 
28
50
  /// Configuration for forking a blockchain
29
51
  #[napi(object)]
30
52
  pub struct ForkConfig {
31
- /// The URL of the JSON-RPC endpoint to fork from
32
- pub json_rpc_url: String,
33
53
  /// The block number to fork from. If not provided, the latest safe block is
34
54
  /// used.
35
55
  pub block_number: Option<BigInt>,
56
+ /// The directory to cache remote JSON-RPC responses
57
+ pub cache_dir: Option<String>,
58
+ /// Overrides for the configuration of chains.
59
+ pub chain_overrides: Option<Vec<ChainOverride>>,
36
60
  /// The HTTP headers to use when making requests to the JSON-RPC endpoint
37
61
  pub http_headers: Option<Vec<HttpHeader>>,
62
+ /// The URL of the JSON-RPC endpoint to fork from
63
+ pub url: String,
38
64
  }
39
65
 
40
66
  #[napi(object)]
@@ -46,10 +72,22 @@ pub struct HttpHeader {
46
72
  /// Configuration for a hardfork activation
47
73
  #[napi(object)]
48
74
  pub struct HardforkActivation {
75
+ /// The condition for the hardfork activation
76
+ pub condition: Either<HardforkActivationByBlockNumber, HardforkActivationByTimestamp>,
77
+ /// The activated hardfork
78
+ pub hardfork: String,
79
+ }
80
+
81
+ #[napi(object)]
82
+ pub struct HardforkActivationByBlockNumber {
49
83
  /// The block number at which the hardfork is activated
50
84
  pub block_number: BigInt,
51
- /// The activated hardfork
52
- pub spec_id: String,
85
+ }
86
+
87
+ #[napi(object)]
88
+ pub struct HardforkActivationByTimestamp {
89
+ /// The timestamp at which the hardfork is activated
90
+ pub timestamp: BigInt,
53
91
  }
54
92
 
55
93
  #[napi(string_enum)]
@@ -81,6 +119,13 @@ pub struct MiningConfig {
81
119
  pub mem_pool: MemPoolConfig,
82
120
  }
83
121
 
122
+ /// Configuration for runtime observability.
123
+ #[napi(object)]
124
+ pub struct ObservabilityConfig {
125
+ /// If present, configures runtime observability to collect code coverage.
126
+ pub code_coverage: Option<CodeCoverageConfig>,
127
+ }
128
+
84
129
  /// Configuration for a provider
85
130
  #[napi(object)]
86
131
  pub struct ProviderConfig {
@@ -94,21 +139,15 @@ pub struct ProviderConfig {
94
139
  pub bail_on_transaction_failure: bool,
95
140
  /// The gas limit of each block
96
141
  pub block_gas_limit: BigInt,
97
- /// The directory to cache remote JSON-RPC responses
98
- pub cache_dir: Option<String>,
99
142
  /// The chain ID of the blockchain
100
143
  pub chain_id: BigInt,
101
- /// The configuration for chains
102
- pub chains: Vec<ChainConfig>,
103
144
  /// The address of the coinbase
104
- pub coinbase: Buffer,
105
- /// Enables RIP-7212
106
- pub enable_rip_7212: bool,
145
+ pub coinbase: Uint8Array,
107
146
  /// The configuration for forking a blockchain. If not provided, a local
108
147
  /// blockchain will be created
109
148
  pub fork: Option<ForkConfig>,
110
149
  /// The genesis state of the blockchain
111
- pub genesis_state: Vec<Account>,
150
+ pub genesis_state: Vec<AccountOverride>,
112
151
  /// The hardfork of the blockchain
113
152
  pub hardfork: String,
114
153
  /// The initial base fee per gas of the blockchain. Required for EIP-1559
@@ -120,22 +159,93 @@ pub struct ProviderConfig {
120
159
  pub initial_date: Option<BigInt>,
121
160
  /// The initial parent beacon block root of the blockchain. Required for
122
161
  /// EIP-4788
123
- pub initial_parent_beacon_block_root: Option<Buffer>,
162
+ pub initial_parent_beacon_block_root: Option<Uint8Array>,
124
163
  /// The minimum gas price of the next block.
125
164
  pub min_gas_price: BigInt,
126
165
  /// The configuration for the miner
127
166
  pub mining: MiningConfig,
128
167
  /// The network ID of the blockchain
129
168
  pub network_id: BigInt,
130
- /// Owned accounts, for which the secret key is known
131
- pub owned_accounts: Vec<OwnedAccount>,
169
+ /// The configuration for the provider's observability
170
+ pub observability: ObservabilityConfig,
171
+ // Using JsString here as it doesn't have `Debug`, `Display` and `Serialize` implementation
172
+ // which prevents accidentally leaking the secret keys to error messages and logs.
173
+ /// Secret keys of owned accounts
174
+ pub owned_accounts: Vec<JsString>,
175
+ /// Overrides for precompiles
176
+ pub precompile_overrides: Vec<Reference<Precompile>>,
132
177
  }
133
178
 
134
- impl TryFrom<ForkConfig> for edr_provider::hardhat_rpc_types::ForkConfig {
179
+ impl TryFrom<ForkConfig> for edr_provider::ForkConfig<String> {
135
180
  type Error = napi::Error;
136
181
 
137
182
  fn try_from(value: ForkConfig) -> Result<Self, Self::Error> {
138
183
  let block_number: Option<u64> = value.block_number.map(TryCast::try_cast).transpose()?;
184
+
185
+ let cache_dir = PathBuf::from(
186
+ value
187
+ .cache_dir
188
+ .unwrap_or(edr_defaults::CACHE_DIR.to_owned()),
189
+ );
190
+
191
+ let chain_overrides = value
192
+ .chain_overrides
193
+ .map(|chain_overrides| {
194
+ chain_overrides
195
+ .into_iter()
196
+ .map(
197
+ |ChainOverride {
198
+ chain_id,
199
+ name,
200
+ hardfork_activation_overrides,
201
+ }| {
202
+ let hardfork_activation_overrides =
203
+ hardfork_activation_overrides
204
+ .map(|hardfork_activations| {
205
+ hardfork_activations
206
+ .into_iter()
207
+ .map(
208
+ |HardforkActivation {
209
+ condition,
210
+ hardfork,
211
+ }| {
212
+ let condition = match condition {
213
+ Either::A(HardforkActivationByBlockNumber {
214
+ block_number,
215
+ }) => edr_evm::hardfork::ForkCondition::Block(
216
+ block_number.try_cast()?,
217
+ ),
218
+ Either::B(HardforkActivationByTimestamp {
219
+ timestamp,
220
+ }) => edr_evm::hardfork::ForkCondition::Timestamp(
221
+ timestamp.try_cast()?,
222
+ ),
223
+ };
224
+
225
+ Ok(edr_evm::hardfork::Activation {
226
+ condition,
227
+ hardfork,
228
+ })
229
+ },
230
+ )
231
+ .collect::<napi::Result<Vec<_>>>()
232
+ .map(edr_evm::hardfork::Activations::new)
233
+ })
234
+ .transpose()?;
235
+
236
+ let chain_config = edr_evm::hardfork::ChainOverride {
237
+ name,
238
+ hardfork_activation_overrides,
239
+ };
240
+
241
+ let chain_id = chain_id.try_cast()?;
242
+ Ok((chain_id, chain_config))
243
+ },
244
+ )
245
+ .collect::<napi::Result<_>>()
246
+ })
247
+ .transpose()?;
248
+
139
249
  let http_headers = value.http_headers.map(|http_headers| {
140
250
  http_headers
141
251
  .into_iter()
@@ -144,9 +254,11 @@ impl TryFrom<ForkConfig> for edr_provider::hardhat_rpc_types::ForkConfig {
144
254
  });
145
255
 
146
256
  Ok(Self {
147
- json_rpc_url: value.json_rpc_url,
148
257
  block_number,
258
+ cache_dir,
259
+ chain_overrides: chain_overrides.unwrap_or_default(),
149
260
  http_headers,
261
+ url: value.url,
150
262
  })
151
263
  }
152
264
  }
@@ -207,133 +319,178 @@ impl TryFrom<MiningConfig> for edr_provider::MiningConfig {
207
319
  }
208
320
  }
209
321
 
210
- impl TryFrom<ProviderConfig> for edr_napi_core::provider::Config {
211
- type Error = napi::Error;
322
+ impl ObservabilityConfig {
323
+ /// Resolves the instance, converting it to a
324
+ /// [`edr_provider::observability::Config`].
325
+ pub fn resolve(
326
+ self,
327
+ env: &napi::Env,
328
+ runtime: runtime::Handle,
329
+ ) -> napi::Result<edr_provider::observability::Config> {
330
+ let on_collected_coverage_fn = self
331
+ .code_coverage
332
+ .map(
333
+ |code_coverage| -> napi::Result<Box<dyn SyncOnCollectedCoverageCallback>> {
334
+ let mut on_collected_coverage_callback: ThreadsafeFunction<
335
+ _,
336
+ ErrorStrategy::Fatal,
337
+ > = code_coverage
338
+ .on_collected_coverage_callback
339
+ .create_threadsafe_function(
340
+ 0,
341
+ |ctx: ThreadSafeCallContext<HashSet<Bytes>>| {
342
+ let hits = ctx
343
+ .env
344
+ .create_array_with_length(ctx.value.len())
345
+ .and_then(|mut hits| {
346
+ for (idx, hit) in ctx.value.into_iter().enumerate() {
347
+ ctx.env
348
+ .create_buffer_with_data(hit.to_vec())
349
+ .and_then(|hit| {
350
+ let idx = u32::try_from(idx).unwrap_or_else(|_| panic!("Number of hits should not exceed '{}'",
351
+ u32::MAX));
352
+
353
+ hits.set_element(idx, hit.into_raw())
354
+ })?;
355
+ }
356
+ Ok(hits)
357
+ })?;
358
+
359
+ Ok(vec![hits])
360
+ },
361
+ )?;
362
+
363
+ // Maintain a weak reference to the function to avoid blocking the event loop
364
+ // from exiting.
365
+ on_collected_coverage_callback.unref(env)?;
366
+
367
+ let on_collected_coverage_fn: Box<dyn SyncOnCollectedCoverageCallback> =
368
+ Box::new(move |hits| {
369
+ let runtime = runtime.clone();
370
+
371
+ let (sender, receiver) = std::sync::mpsc::channel();
372
+
373
+ let status = on_collected_coverage_callback
374
+ .call_with_return_value(hits, ThreadsafeFunctionCallMode::Blocking, move |result: Promise<()>| {
375
+ // We spawn a background task to handle the async callback
376
+ runtime.spawn(async move {
377
+ let result = result.await;
378
+ sender.send(result).map_err(|_error| {
379
+ napi::Error::new(
380
+ napi::Status::GenericFailure,
381
+ "Failed to send result from on_collected_coverage_callback",
382
+ )
383
+ })
384
+ });
385
+ Ok(())
386
+ });
387
+
388
+ let () = receiver.recv().expect("Receive can only fail if the channel is closed")?;
389
+
390
+ assert_eq!(status, napi::Status::Ok);
391
+
392
+ Ok(())
393
+ });
394
+
395
+ Ok(on_collected_coverage_fn)
396
+ },
397
+ )
398
+ .transpose()?;
399
+
400
+ Ok(edr_provider::observability::Config {
401
+ on_collected_coverage_fn,
402
+ ..edr_provider::observability::Config::default()
403
+ })
404
+ }
405
+ }
212
406
 
213
- fn try_from(value: ProviderConfig) -> Result<Self, Self::Error> {
214
- let accounts = value
407
+ impl ProviderConfig {
408
+ /// Resolves the instance to a [`edr_napi_core::provider::Config`].
409
+ pub fn resolve(
410
+ self,
411
+ env: &napi::Env,
412
+ runtime: runtime::Handle,
413
+ ) -> napi::Result<edr_napi_core::provider::Config> {
414
+ let owned_accounts = self
215
415
  .owned_accounts
216
416
  .into_iter()
217
- .map(edr_provider::config::OwnedAccount::try_from)
417
+ .map(|secret_key| {
418
+ // This is the only place in production code where it's allowed to use
419
+ // `DangerousSecretKeyStr`.
420
+ #[allow(deprecated)]
421
+ use edr_eth::signature::DangerousSecretKeyStr;
422
+
423
+ static_assertions::assert_not_impl_all!(JsString: Debug, Display, serde::Serialize);
424
+ static_assertions::assert_not_impl_all!(JsStringUtf8: Debug, Display, serde::Serialize);
425
+ // `SecretKey` has `Debug` implementation, but it's opaque (only shows the
426
+ // type name)
427
+ static_assertions::assert_not_impl_any!(SecretKey: Display, serde::Serialize);
428
+
429
+ let secret_key = secret_key.into_utf8()?;
430
+ // This is the only place in production code where it's allowed to use
431
+ // `DangerousSecretKeyStr`.
432
+ #[allow(deprecated)]
433
+ let secret_key_str = DangerousSecretKeyStr(secret_key.as_str()?);
434
+ let secret_key: SecretKey = secret_key_from_str(secret_key_str)
435
+ .map_err(|error| napi::Error::new(napi::Status::InvalidArg, error))?;
436
+
437
+ Ok(secret_key)
438
+ })
218
439
  .collect::<napi::Result<Vec<_>>>()?;
219
440
 
220
441
  let block_gas_limit =
221
- NonZeroU64::new(value.block_gas_limit.try_cast()?).ok_or_else(|| {
442
+ NonZeroU64::new(self.block_gas_limit.try_cast()?).ok_or_else(|| {
222
443
  napi::Error::new(
223
444
  napi::Status::GenericFailure,
224
445
  "Block gas limit must be greater than 0",
225
446
  )
226
447
  })?;
227
448
 
228
- let chains = value
229
- .chains
230
- .into_iter()
231
- .map(
232
- |ChainConfig {
233
- chain_id,
234
- hardforks,
235
- }| {
236
- let hardforks = hardforks
237
- .into_iter()
238
- .map(
239
- |HardforkActivation {
240
- block_number,
241
- spec_id: hardfork,
242
- }| {
243
- let block_number = block_number.try_cast()?;
244
-
245
- Ok(edr_napi_core::provider::HardforkActivation {
246
- block_number,
247
- hardfork,
248
- })
249
- },
250
- )
251
- .collect::<napi::Result<Vec<_>>>()?;
252
-
253
- let chain_id = chain_id.try_cast()?;
254
- Ok((chain_id, hardforks))
255
- },
256
- )
257
- .collect::<napi::Result<_>>()?;
258
-
259
- let genesis_state = value
449
+ let genesis_state = self
260
450
  .genesis_state
261
451
  .into_iter()
262
- .map(
263
- |Account {
264
- address,
265
- balance,
266
- nonce,
267
- code,
268
- storage,
269
- }| {
270
- let code: Option<edr_eth::Bytecode> =
271
- code.map(TryCast::try_cast).transpose()?;
272
-
273
- let code_hash = code
274
- .as_ref()
275
- .map_or(KECCAK_EMPTY, edr_eth::Bytecode::hash_slow);
276
-
277
- let info = edr_eth::account::AccountInfo {
278
- balance: balance.try_cast()?,
279
- nonce: nonce.try_cast()?,
280
- code_hash,
281
- code,
282
- };
283
-
284
- let storage = storage
285
- .into_iter()
286
- .map(|StorageSlot { index, value }| {
287
- let value = value.try_cast()?;
288
- let slot = edr_evm::state::EvmStorageSlot::new(value);
289
-
290
- let index: edr_eth::U256 = index.try_cast()?;
291
- Ok((index, slot))
292
- })
293
- .collect::<napi::Result<_>>()?;
294
-
295
- let address: edr_eth::Address = address.try_cast()?;
296
- let account = edr_provider::config::Account { info, storage };
452
+ .map(TryInto::try_into)
453
+ .collect::<napi::Result<HashMap<edr_eth::Address, edr_provider::AccountOverride>>>()?;
297
454
 
298
- Ok((address, account))
299
- },
300
- )
301
- .collect::<napi::Result<_>>()?;
302
-
303
- Ok(Self {
304
- accounts,
305
- allow_blocks_with_same_timestamp: value.allow_blocks_with_same_timestamp,
306
- allow_unlimited_contract_size: value.allow_unlimited_contract_size,
307
- bail_on_call_failure: value.bail_on_call_failure,
308
- bail_on_transaction_failure: value.bail_on_transaction_failure,
455
+ let precompile_overrides = self
456
+ .precompile_overrides
457
+ .into_iter()
458
+ .map(|precompile| precompile.to_tuple())
459
+ .collect();
460
+
461
+ Ok(edr_napi_core::provider::Config {
462
+ allow_blocks_with_same_timestamp: self.allow_blocks_with_same_timestamp,
463
+ allow_unlimited_contract_size: self.allow_unlimited_contract_size,
464
+ bail_on_call_failure: self.bail_on_call_failure,
465
+ bail_on_transaction_failure: self.bail_on_transaction_failure,
309
466
  block_gas_limit,
310
- cache_dir: value.cache_dir,
311
- chain_id: value.chain_id.try_cast()?,
312
- chains,
313
- coinbase: value.coinbase.try_cast()?,
314
- enable_rip_7212: value.enable_rip_7212,
315
- fork: value.fork.map(TryInto::try_into).transpose()?,
467
+ chain_id: self.chain_id.try_cast()?,
468
+ coinbase: self.coinbase.try_cast()?,
469
+ fork: self.fork.map(TryInto::try_into).transpose()?,
316
470
  genesis_state,
317
- hardfork: value.hardfork,
318
- initial_base_fee_per_gas: value
471
+ hardfork: self.hardfork,
472
+ initial_base_fee_per_gas: self
319
473
  .initial_base_fee_per_gas
320
474
  .map(TryCast::try_cast)
321
475
  .transpose()?,
322
- initial_blob_gas: value.initial_blob_gas.map(TryInto::try_into).transpose()?,
323
- initial_date: value
476
+ initial_blob_gas: self.initial_blob_gas.map(TryInto::try_into).transpose()?,
477
+ initial_date: self
324
478
  .initial_date
325
479
  .map(|date| {
326
480
  let elapsed_since_epoch = Duration::from_secs(date.try_cast()?);
327
481
  napi::Result::Ok(SystemTime::UNIX_EPOCH + elapsed_since_epoch)
328
482
  })
329
483
  .transpose()?,
330
- initial_parent_beacon_block_root: value
484
+ initial_parent_beacon_block_root: self
331
485
  .initial_parent_beacon_block_root
332
486
  .map(TryCast::try_cast)
333
487
  .transpose()?,
334
- mining: value.mining.try_into()?,
335
- min_gas_price: value.min_gas_price.try_cast()?,
336
- network_id: value.network_id.try_cast()?,
488
+ mining: self.mining.try_into()?,
489
+ min_gas_price: self.min_gas_price.try_cast()?,
490
+ network_id: self.network_id.try_cast()?,
491
+ observability: self.observability.resolve(env, runtime)?,
492
+ owned_accounts,
493
+ precompile_overrides,
337
494
  })
338
495
  }
339
496
  }
@@ -349,6 +506,23 @@ pub struct TracingConfigWithBuffers {
349
506
  pub ignore_contracts: Option<bool>,
350
507
  }
351
508
 
509
+ impl From<TracingConfigWithBuffers> for edr_napi_core::solidity::config::TracingConfigWithBuffers {
510
+ fn from(value: TracingConfigWithBuffers) -> Self {
511
+ edr_napi_core::solidity::config::TracingConfigWithBuffers {
512
+ build_infos: value.build_infos.map(|infos| match infos {
513
+ Either::A(with_output) => Either::A(with_output),
514
+ Either::B(separate_output) => Either::B(
515
+ separate_output
516
+ .into_iter()
517
+ .map(edr_napi_core::solidity::config::BuildInfoAndOutput::from)
518
+ .collect(),
519
+ ),
520
+ }),
521
+ ignore_contracts: value.ignore_contracts,
522
+ }
523
+ }
524
+ }
525
+
352
526
  /// Hardhat V3 build info where the compiler output is not part of the build
353
527
  /// info file.
354
528
  #[napi(object)]
@@ -359,41 +533,11 @@ pub struct BuildInfoAndOutput {
359
533
  pub output: Uint8Array,
360
534
  }
361
535
 
362
- impl<'a> From<&'a BuildInfoAndOutput>
363
- for edr_solidity::artifacts::BuildInfoBufferSeparateOutput<'a>
364
- {
365
- fn from(value: &'a BuildInfoAndOutput) -> Self {
536
+ impl From<BuildInfoAndOutput> for edr_napi_core::solidity::config::BuildInfoAndOutput {
537
+ fn from(value: BuildInfoAndOutput) -> Self {
366
538
  Self {
367
- build_info: value.build_info.as_ref(),
368
- output: value.output.as_ref(),
369
- }
370
- }
371
- }
372
-
373
- impl<'a> From<&'a TracingConfigWithBuffers>
374
- for edr_solidity::artifacts::BuildInfoConfigWithBuffers<'a>
375
- {
376
- fn from(value: &'a TracingConfigWithBuffers) -> Self {
377
- use edr_solidity::artifacts::{BuildInfoBufferSeparateOutput, BuildInfoBuffers};
378
-
379
- let build_infos = value.build_infos.as_ref().map(|infos| match infos {
380
- Either::A(with_output) => BuildInfoBuffers::WithOutput(
381
- with_output
382
- .iter()
383
- .map(std::convert::AsRef::as_ref)
384
- .collect(),
385
- ),
386
- Either::B(separate_output) => BuildInfoBuffers::SeparateInputOutput(
387
- separate_output
388
- .iter()
389
- .map(BuildInfoBufferSeparateOutput::from)
390
- .collect(),
391
- ),
392
- });
393
-
394
- Self {
395
- build_infos,
396
- ignore_contracts: value.ignore_contracts,
539
+ build_info: value.build_info,
540
+ output: value.output,
397
541
  }
398
542
  }
399
543
  }