@nomicfoundation/edr 0.2.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.cargo/config.toml +8 -0
  2. package/.mocharc.json +4 -0
  3. package/Cargo.toml +50 -0
  4. package/LICENSE +1 -0
  5. package/artifacts/bindings-aarch64-apple-darwin/edr.darwin-arm64.node +0 -0
  6. package/artifacts/bindings-aarch64-pc-windows-msvc/edr.win32-arm64-msvc.node +0 -0
  7. package/artifacts/bindings-aarch64-unknown-linux-gnu/edr.linux-arm64-gnu.node +0 -0
  8. package/artifacts/bindings-aarch64-unknown-linux-musl/edr.linux-arm64-musl.node +0 -0
  9. package/artifacts/bindings-i686-pc-windows-msvc/edr.win32-ia32-msvc.node +0 -0
  10. package/artifacts/bindings-x86_64-apple-darwin/edr.darwin-x64.node +0 -0
  11. package/artifacts/bindings-x86_64-pc-windows-msvc/edr.win32-x64-msvc.node +0 -0
  12. package/artifacts/bindings-x86_64-unknown-linux-gnu/edr.linux-x64-gnu.node +0 -0
  13. package/artifacts/bindings-x86_64-unknown-linux-musl/edr.linux-x64-musl.node +0 -0
  14. package/build.rs +3 -0
  15. package/index.d.ts +383 -0
  16. package/index.js +264 -0
  17. package/npm/darwin-arm64/README.md +3 -0
  18. package/npm/darwin-arm64/edr.darwin-arm64.node +0 -0
  19. package/npm/darwin-arm64/package.json +22 -0
  20. package/npm/darwin-x64/README.md +3 -0
  21. package/npm/darwin-x64/edr.darwin-x64.node +0 -0
  22. package/npm/darwin-x64/package.json +22 -0
  23. package/npm/linux-arm64-gnu/README.md +3 -0
  24. package/npm/linux-arm64-gnu/edr.linux-arm64-gnu.node +0 -0
  25. package/npm/linux-arm64-gnu/package.json +25 -0
  26. package/npm/linux-arm64-musl/README.md +3 -0
  27. package/npm/linux-arm64-musl/edr.linux-arm64-musl.node +0 -0
  28. package/npm/linux-arm64-musl/package.json +25 -0
  29. package/npm/linux-x64-gnu/README.md +3 -0
  30. package/npm/linux-x64-gnu/edr.linux-x64-gnu.node +0 -0
  31. package/npm/linux-x64-gnu/package.json +25 -0
  32. package/npm/linux-x64-musl/README.md +3 -0
  33. package/npm/linux-x64-musl/edr.linux-x64-musl.node +0 -0
  34. package/npm/linux-x64-musl/package.json +25 -0
  35. package/npm/win32-arm64-msvc/README.md +3 -0
  36. package/npm/win32-arm64-msvc/edr.win32-arm64-msvc.node +0 -0
  37. package/npm/win32-arm64-msvc/package.json +22 -0
  38. package/npm/win32-ia32-msvc/README.md +3 -0
  39. package/npm/win32-ia32-msvc/edr.win32-ia32-msvc.node +0 -0
  40. package/npm/win32-ia32-msvc/package.json +22 -0
  41. package/npm/win32-x64-msvc/README.md +3 -0
  42. package/npm/win32-x64-msvc/edr.win32-x64-msvc.node +0 -0
  43. package/npm/win32-x64-msvc/package.json +22 -0
  44. package/package.json +61 -0
  45. package/src/account.rs +28 -0
  46. package/src/block.rs +110 -0
  47. package/src/cast.rs +119 -0
  48. package/src/config.rs +70 -0
  49. package/src/context.rs +74 -0
  50. package/src/debug_trace.rs +38 -0
  51. package/src/lib.rs +18 -0
  52. package/src/log.rs +41 -0
  53. package/src/logger.rs +1277 -0
  54. package/src/provider/config.rs +271 -0
  55. package/src/provider.rs +185 -0
  56. package/src/result.rs +254 -0
  57. package/src/subscribe.rs +60 -0
  58. package/src/sync.rs +85 -0
  59. package/src/threadsafe_function.rs +305 -0
  60. package/src/trace.rs +168 -0
  61. package/test/provider.ts +104 -0
  62. package/tsconfig.json +17 -0
package/src/logger.rs ADDED
@@ -0,0 +1,1277 @@
1
+ use std::{
2
+ fmt::Display,
3
+ sync::mpsc::{channel, Sender},
4
+ };
5
+
6
+ use ansi_term::{Color, Style};
7
+ use edr_eth::{Bytes, B256, U256};
8
+ use edr_evm::{
9
+ blockchain::BlockchainError,
10
+ precompile::{self, Precompiles},
11
+ trace::TraceMessage,
12
+ ExecutableTransaction, ExecutionResult, SyncBlock,
13
+ };
14
+ use edr_provider::{ProviderError, TransactionFailure};
15
+ use itertools::izip;
16
+ use napi::{Env, JsFunction, NapiRaw, Status};
17
+ use napi_derive::napi;
18
+
19
+ use crate::{
20
+ cast::TryCast,
21
+ sync::{await_promise, handle_error},
22
+ threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode},
23
+ };
24
+
25
+ #[napi(object)]
26
+ pub struct ContractAndFunctionName {
27
+ /// The contract name.
28
+ pub contract_name: String,
29
+ /// The function name. Only present for calls.
30
+ pub function_name: Option<String>,
31
+ }
32
+
33
+ impl TryCast<(String, Option<String>)> for ContractAndFunctionName {
34
+ type Error = napi::Error;
35
+
36
+ fn try_cast(self) -> std::result::Result<(String, Option<String>), Self::Error> {
37
+ Ok((self.contract_name, self.function_name))
38
+ }
39
+ }
40
+
41
+ struct DecodeConsoleLogInputsCall {
42
+ inputs: Vec<Bytes>,
43
+ sender: Sender<napi::Result<Vec<String>>>,
44
+ }
45
+
46
+ struct ContractAndFunctionNameCall {
47
+ code: Bytes,
48
+ /// Only present for calls.
49
+ calldata: Option<Bytes>,
50
+ sender: Sender<napi::Result<(String, Option<String>)>>,
51
+ }
52
+
53
+ #[napi(object)]
54
+ pub struct LoggerConfig {
55
+ /// Whether to enable the logger.
56
+ pub enable: bool,
57
+ #[napi(ts_type = "(inputs: Buffer[]) => string[]")]
58
+ pub decode_console_log_inputs_callback: JsFunction,
59
+ #[napi(ts_type = "(code: Buffer, calldata?: Buffer) => ContractAndFunctionName")]
60
+ pub get_contract_and_function_name_callback: JsFunction,
61
+ #[napi(ts_type = "(message: string, replace: boolean) => void")]
62
+ pub print_line_callback: JsFunction,
63
+ }
64
+
65
+ #[derive(Clone)]
66
+ pub enum LoggingState {
67
+ CollapsingMethod(CollapsedMethod),
68
+ HardhatMinining {
69
+ empty_blocks_range_start: Option<u64>,
70
+ },
71
+ IntervalMining {
72
+ empty_blocks_range_start: Option<u64>,
73
+ },
74
+ Empty,
75
+ }
76
+
77
+ impl LoggingState {
78
+ /// Converts the state into a hardhat mining state.
79
+ pub fn into_hardhat_mining(self) -> Option<u64> {
80
+ match self {
81
+ Self::HardhatMinining {
82
+ empty_blocks_range_start,
83
+ } => empty_blocks_range_start,
84
+ _ => None,
85
+ }
86
+ }
87
+
88
+ /// Converts the state into an interval mining state.
89
+ pub fn into_interval_mining(self) -> Option<u64> {
90
+ match self {
91
+ Self::IntervalMining {
92
+ empty_blocks_range_start,
93
+ } => empty_blocks_range_start,
94
+ _ => None,
95
+ }
96
+ }
97
+ }
98
+
99
+ impl Default for LoggingState {
100
+ fn default() -> Self {
101
+ Self::Empty
102
+ }
103
+ }
104
+
105
+ #[derive(Clone)]
106
+ enum LogLine {
107
+ Single(String),
108
+ WithTitle(String, String),
109
+ }
110
+
111
+ #[derive(Debug, thiserror::Error)]
112
+ pub enum LoggerError {
113
+ #[error("Failed to print line")]
114
+ PrintLine,
115
+ }
116
+
117
+ #[derive(Clone)]
118
+ pub struct Logger {
119
+ collector: LogCollector,
120
+ }
121
+
122
+ impl Logger {
123
+ pub fn new(env: &Env, config: LoggerConfig) -> napi::Result<Self> {
124
+ Ok(Self {
125
+ collector: LogCollector::new(env, config)?,
126
+ })
127
+ }
128
+ }
129
+
130
+ impl edr_provider::Logger for Logger {
131
+ type BlockchainError = BlockchainError;
132
+
133
+ type LoggerError = LoggerError;
134
+
135
+ fn is_enabled(&self) -> bool {
136
+ self.collector.is_enabled
137
+ }
138
+
139
+ fn set_is_enabled(&mut self, is_enabled: bool) {
140
+ self.collector.is_enabled = is_enabled;
141
+ }
142
+
143
+ fn log_call(
144
+ &mut self,
145
+ spec_id: edr_eth::SpecId,
146
+ transaction: &ExecutableTransaction,
147
+ result: &edr_provider::CallResult,
148
+ ) -> Result<(), Self::LoggerError> {
149
+ self.collector.log_call(spec_id, transaction, result);
150
+
151
+ Ok(())
152
+ }
153
+
154
+ fn log_estimate_gas_failure(
155
+ &mut self,
156
+ spec_id: edr_eth::SpecId,
157
+ transaction: &ExecutableTransaction,
158
+ failure: &edr_provider::EstimateGasFailure,
159
+ ) -> Result<(), Self::LoggerError> {
160
+ self.collector
161
+ .log_estimate_gas(spec_id, transaction, failure);
162
+
163
+ Ok(())
164
+ }
165
+
166
+ fn log_interval_mined(
167
+ &mut self,
168
+ spec_id: edr_eth::SpecId,
169
+ mining_result: &edr_provider::DebugMineBlockResult<Self::BlockchainError>,
170
+ ) -> Result<(), Self::LoggerError> {
171
+ self.collector.log_interval_mined(spec_id, mining_result)
172
+ }
173
+
174
+ fn log_mined_block(
175
+ &mut self,
176
+ spec_id: edr_eth::SpecId,
177
+ mining_results: &[edr_provider::DebugMineBlockResult<Self::BlockchainError>],
178
+ ) -> Result<(), Self::LoggerError> {
179
+ self.collector.log_mined_blocks(spec_id, mining_results);
180
+
181
+ Ok(())
182
+ }
183
+
184
+ fn log_send_transaction(
185
+ &mut self,
186
+ spec_id: edr_eth::SpecId,
187
+ transaction: &edr_evm::ExecutableTransaction,
188
+ mining_results: &[edr_provider::DebugMineBlockResult<Self::BlockchainError>],
189
+ ) -> Result<(), Self::LoggerError> {
190
+ self.collector
191
+ .log_send_transaction(spec_id, transaction, mining_results);
192
+
193
+ Ok(())
194
+ }
195
+
196
+ fn print_method_logs(
197
+ &mut self,
198
+ method: &str,
199
+ error: Option<&ProviderError<LoggerError>>,
200
+ ) -> Result<(), Self::LoggerError> {
201
+ if let Some(error) = error {
202
+ self.collector.state = LoggingState::Empty;
203
+
204
+ if matches!(error, ProviderError::UnsupportedMethod { .. }) {
205
+ self.collector
206
+ .print::<false>(Color::Red.paint(error.to_string()))?;
207
+ } else {
208
+ self.collector.print::<false>(Color::Red.paint(method))?;
209
+ self.collector.print_logs()?;
210
+
211
+ if !matches!(error, ProviderError::TransactionFailed(_)) {
212
+ self.collector.print_empty_line()?;
213
+
214
+ let error_message = error.to_string();
215
+ self.collector
216
+ .try_indented(|logger| logger.print::<false>(&error_message))?;
217
+
218
+ if matches!(error, ProviderError::InvalidEip155TransactionChainId) {
219
+ self.collector.try_indented(|logger| {
220
+ logger.print::<false>(Color::Yellow.paint(
221
+ "If you are using MetaMask, you can learn how to fix this error here: https://hardhat.org/metamask-issue"
222
+ ))
223
+ })?;
224
+ }
225
+ }
226
+
227
+ self.collector.print_empty_line()?;
228
+ }
229
+ } else {
230
+ self.collector.print_method(method)?;
231
+
232
+ let printed = self.collector.print_logs()?;
233
+ if printed {
234
+ self.collector.print_empty_line()?;
235
+ }
236
+ }
237
+
238
+ Ok(())
239
+ }
240
+ }
241
+
242
+ #[derive(Clone)]
243
+ pub struct CollapsedMethod {
244
+ count: usize,
245
+ method: String,
246
+ }
247
+
248
+ #[derive(Clone)]
249
+ struct LogCollector {
250
+ decode_console_log_inputs_fn: ThreadsafeFunction<DecodeConsoleLogInputsCall>,
251
+ get_contract_and_function_name_fn: ThreadsafeFunction<ContractAndFunctionNameCall>,
252
+ indentation: usize,
253
+ is_enabled: bool,
254
+ logs: Vec<LogLine>,
255
+ print_line_fn: ThreadsafeFunction<(String, bool)>,
256
+ state: LoggingState,
257
+ title_length: usize,
258
+ }
259
+
260
+ impl LogCollector {
261
+ pub fn new(env: &Env, config: LoggerConfig) -> napi::Result<Self> {
262
+ let decode_console_log_inputs_fn = ThreadsafeFunction::create(
263
+ env.raw(),
264
+ // SAFETY: The callback is guaranteed to be valid for the lifetime of the tracer.
265
+ unsafe { config.decode_console_log_inputs_callback.raw() },
266
+ 0,
267
+ |ctx: ThreadSafeCallContext<DecodeConsoleLogInputsCall>| {
268
+ // Bytes[]
269
+ let inputs = ctx
270
+ .env
271
+ .create_array_with_length(ctx.value.inputs.len())
272
+ .and_then(|mut inputs| {
273
+ for (idx, input) in ctx.value.inputs.into_iter().enumerate() {
274
+ // SAFETY: The input is guaranteed to be valid for the lifetime of the
275
+ // JS buffer.
276
+ unsafe {
277
+ ctx.env.create_buffer_with_borrowed_data(
278
+ input.as_ptr(),
279
+ input.len(),
280
+ input,
281
+ |input: Bytes, _env| {
282
+ std::mem::drop(input);
283
+ },
284
+ )
285
+ }
286
+ .and_then(|input| inputs.set_element(idx as u32, input.into_raw()))?;
287
+ }
288
+
289
+ Ok(inputs)
290
+ })?;
291
+
292
+ let sender = ctx.value.sender.clone();
293
+
294
+ let promise = ctx.callback.call(None, &[inputs])?;
295
+ let result =
296
+ await_promise::<Vec<String>, Vec<String>>(ctx.env, promise, ctx.value.sender);
297
+
298
+ handle_error(sender, result)
299
+ },
300
+ )?;
301
+
302
+ let get_contract_and_function_name_fn = ThreadsafeFunction::create(
303
+ env.raw(),
304
+ // SAFETY: The callback is guaranteed to be valid for the lifetime of the tracer.
305
+ unsafe { config.get_contract_and_function_name_callback.raw() },
306
+ 0,
307
+ |ctx: ThreadSafeCallContext<ContractAndFunctionNameCall>| {
308
+ // Buffer
309
+ let code = ctx.value.code;
310
+ // SAFETY: The code is guaranteed to be valid for the lifetime of the
311
+ // JS buffer.
312
+ let code = unsafe {
313
+ ctx.env.create_buffer_with_borrowed_data(
314
+ code.as_ptr(),
315
+ code.len(),
316
+ code,
317
+ |code: Bytes, _env| {
318
+ std::mem::drop(code);
319
+ },
320
+ )
321
+ }?
322
+ .into_unknown();
323
+
324
+ // Option<Buffer>
325
+ let calldata = if let Some(calldata) = ctx.value.calldata {
326
+ // SAFETY: The calldata is guaranteed to be valid for the lifetime of the
327
+ // JS buffer.
328
+ unsafe {
329
+ ctx.env.create_buffer_with_borrowed_data(
330
+ calldata.as_ptr(),
331
+ calldata.len(),
332
+ calldata,
333
+ |calldata: Bytes, _env| {
334
+ std::mem::drop(calldata);
335
+ },
336
+ )
337
+ }?
338
+ .into_unknown()
339
+ } else {
340
+ ctx.env.get_undefined()?.into_unknown()
341
+ };
342
+
343
+ let sender = ctx.value.sender.clone();
344
+
345
+ let promise = ctx.callback.call(None, &[code, calldata])?;
346
+ let result = await_promise::<ContractAndFunctionName, (String, Option<String>)>(
347
+ ctx.env,
348
+ promise,
349
+ ctx.value.sender,
350
+ );
351
+
352
+ handle_error(sender, result)
353
+ },
354
+ )?;
355
+
356
+ let print_line_fn = ThreadsafeFunction::create(
357
+ env.raw(),
358
+ // SAFETY: The callback is guaranteed to be valid for the lifetime of the tracer.
359
+ unsafe { config.print_line_callback.raw() },
360
+ 0,
361
+ |ctx: ThreadSafeCallContext<(String, bool)>| {
362
+ // String
363
+ let message = ctx.env.create_string_from_std(ctx.value.0)?;
364
+
365
+ // bool
366
+ let replace = ctx.env.get_boolean(ctx.value.1)?;
367
+
368
+ ctx.callback
369
+ .call(None, &[message.into_unknown(), replace.into_unknown()])?;
370
+ Ok(())
371
+ },
372
+ )?;
373
+
374
+ Ok(Self {
375
+ decode_console_log_inputs_fn,
376
+ get_contract_and_function_name_fn,
377
+ indentation: 0,
378
+ is_enabled: config.enable,
379
+ logs: Vec::new(),
380
+ print_line_fn,
381
+ state: LoggingState::default(),
382
+ title_length: 0,
383
+ })
384
+ }
385
+
386
+ pub fn log_call(
387
+ &mut self,
388
+ spec_id: edr_eth::SpecId,
389
+ transaction: &ExecutableTransaction,
390
+ result: &edr_provider::CallResult,
391
+ ) {
392
+ let edr_provider::CallResult {
393
+ console_log_inputs,
394
+ execution_result,
395
+ trace,
396
+ } = result;
397
+
398
+ self.state = LoggingState::Empty;
399
+
400
+ self.indented(|logger| {
401
+ logger.log_contract_and_function_name::<true>(spec_id, trace);
402
+
403
+ logger.log_with_title("From", format!("0x{:x}", transaction.caller()));
404
+ if let Some(to) = transaction.to() {
405
+ logger.log_with_title("To", format!("0x{to:x}"));
406
+ }
407
+ if transaction.value() > U256::ZERO {
408
+ logger.log_with_title("Value", wei_to_human_readable(transaction.value()));
409
+ }
410
+
411
+ logger.log_console_log_messages(console_log_inputs);
412
+
413
+ if let Some(transaction_failure) = TransactionFailure::from_execution_result(
414
+ execution_result,
415
+ transaction.hash(),
416
+ trace,
417
+ ) {
418
+ logger.log_transaction_failure(&transaction_failure);
419
+ }
420
+ });
421
+ }
422
+
423
+ pub fn log_estimate_gas(
424
+ &mut self,
425
+ spec_id: edr_eth::SpecId,
426
+ transaction: &ExecutableTransaction,
427
+ result: &edr_provider::EstimateGasFailure,
428
+ ) {
429
+ let edr_provider::EstimateGasFailure {
430
+ console_log_inputs,
431
+ transaction_failure,
432
+ } = result;
433
+
434
+ self.state = LoggingState::Empty;
435
+
436
+ self.indented(|logger| {
437
+ logger.log_contract_and_function_name::<true>(
438
+ spec_id,
439
+ &transaction_failure.failure.solidity_trace,
440
+ );
441
+
442
+ logger.log_with_title("From", format!("0x{:x}", transaction.caller()));
443
+ if let Some(to) = transaction.to() {
444
+ logger.log_with_title("To", format!("0x{to:x}"));
445
+ }
446
+ logger.log_with_title("Value", wei_to_human_readable(transaction.value()));
447
+
448
+ logger.log_console_log_messages(console_log_inputs);
449
+
450
+ logger.log_transaction_failure(&transaction_failure.failure);
451
+ });
452
+ }
453
+
454
+ fn log_transaction_failure(&mut self, failure: &edr_provider::TransactionFailure) {
455
+ let is_revert_error = matches!(
456
+ failure.reason,
457
+ edr_provider::TransactionFailureReason::Revert(_)
458
+ );
459
+
460
+ let error_type = if is_revert_error {
461
+ "Error"
462
+ } else {
463
+ "TransactionExecutionError"
464
+ };
465
+
466
+ self.log_empty_line();
467
+ self.log(format!("{error_type}: {failure}"));
468
+ }
469
+
470
+ pub fn log_mined_blocks(
471
+ &mut self,
472
+ spec_id: edr_eth::SpecId,
473
+ mining_results: &[edr_provider::DebugMineBlockResult<BlockchainError>],
474
+ ) {
475
+ let num_results = mining_results.len();
476
+ for (idx, mining_result) in mining_results.iter().enumerate() {
477
+ let state = std::mem::take(&mut self.state);
478
+ let empty_blocks_range_start = state.into_hardhat_mining();
479
+
480
+ if mining_result.block.transactions().is_empty() {
481
+ self.log_hardhat_mined_empty_block(&mining_result.block, empty_blocks_range_start);
482
+
483
+ let block_number = mining_result.block.header().number;
484
+ self.state = LoggingState::HardhatMinining {
485
+ empty_blocks_range_start: Some(
486
+ empty_blocks_range_start.unwrap_or(block_number),
487
+ ),
488
+ };
489
+ } else {
490
+ self.log_hardhat_mined_block(spec_id, mining_result);
491
+
492
+ if idx < num_results - 1 {
493
+ self.log_empty_line();
494
+ }
495
+ }
496
+ }
497
+ }
498
+
499
+ pub fn log_interval_mined(
500
+ &mut self,
501
+ spec_id: edr_eth::SpecId,
502
+ mining_result: &edr_provider::DebugMineBlockResult<BlockchainError>,
503
+ ) -> Result<(), LoggerError> {
504
+ let block_header = mining_result.block.header();
505
+ let block_number = block_header.number;
506
+
507
+ if mining_result.block.transactions().is_empty() {
508
+ let state = std::mem::take(&mut self.state);
509
+ let empty_blocks_range_start = state.into_interval_mining();
510
+
511
+ if let Some(empty_blocks_range_start) = empty_blocks_range_start {
512
+ self.print::<true>(format!(
513
+ "Mined empty block range #{empty_blocks_range_start} to #{block_number}"
514
+ ))?;
515
+ } else {
516
+ let base_fee = if let Some(base_fee) = block_header.base_fee_per_gas.as_ref() {
517
+ format!(" with base fee {base_fee}")
518
+ } else {
519
+ String::new()
520
+ };
521
+
522
+ self.print::<false>(format!("Mined empty block #{block_number}{base_fee}"))?;
523
+ }
524
+
525
+ self.state = LoggingState::IntervalMining {
526
+ empty_blocks_range_start: Some(
527
+ empty_blocks_range_start.unwrap_or(block_header.number),
528
+ ),
529
+ };
530
+ } else {
531
+ self.log_interval_mined_block(spec_id, mining_result);
532
+
533
+ self.print::<false>(format!("Mined block #{block_number}"))?;
534
+
535
+ let printed = self.print_logs()?;
536
+ if printed {
537
+ self.print_empty_line()?;
538
+ }
539
+ }
540
+
541
+ Ok(())
542
+ }
543
+
544
+ pub fn log_send_transaction(
545
+ &mut self,
546
+ spec_id: edr_eth::SpecId,
547
+ transaction: &edr_evm::ExecutableTransaction,
548
+ mining_results: &[edr_provider::DebugMineBlockResult<BlockchainError>],
549
+ ) {
550
+ if !mining_results.is_empty() {
551
+ self.state = LoggingState::Empty;
552
+
553
+ let (sent_block_result, sent_transaction_result, sent_trace) = mining_results
554
+ .iter()
555
+ .find_map(|result| {
556
+ izip!(
557
+ result.block.transactions(),
558
+ result.transaction_results.iter(),
559
+ result.transaction_traces.iter()
560
+ )
561
+ .find(|(block_transaction, _, _)| {
562
+ *block_transaction.hash() == *transaction.hash()
563
+ })
564
+ .map(|(_, transaction_result, trace)| (result, transaction_result, trace))
565
+ })
566
+ .expect("Transaction result not found");
567
+
568
+ if mining_results.len() > 1 {
569
+ self.log_multiple_blocks_warning();
570
+ self.log_auto_mined_block_results(spec_id, mining_results, transaction.hash());
571
+ self.log_currently_sent_transaction(
572
+ spec_id,
573
+ sent_block_result,
574
+ transaction,
575
+ sent_transaction_result,
576
+ sent_trace,
577
+ );
578
+ } else if let Some(result) = mining_results.first() {
579
+ let transactions = result.block.transactions();
580
+ if transactions.len() > 1 {
581
+ self.log_multiple_transactions_warning();
582
+ self.log_auto_mined_block_results(spec_id, mining_results, transaction.hash());
583
+ self.log_currently_sent_transaction(
584
+ spec_id,
585
+ sent_block_result,
586
+ transaction,
587
+ sent_transaction_result,
588
+ sent_trace,
589
+ );
590
+ } else if let Some(transaction) = transactions.first() {
591
+ self.log_single_transaction_mining_result(spec_id, result, transaction);
592
+ }
593
+ }
594
+ }
595
+ }
596
+
597
+ fn contract_and_function_name(
598
+ &self,
599
+ code: Bytes,
600
+ calldata: Option<Bytes>,
601
+ ) -> (String, Option<String>) {
602
+ let (sender, receiver) = channel();
603
+
604
+ let status = self.get_contract_and_function_name_fn.call(
605
+ ContractAndFunctionNameCall {
606
+ code,
607
+ calldata,
608
+ sender,
609
+ },
610
+ ThreadsafeFunctionCallMode::Blocking,
611
+ );
612
+ assert_eq!(status, Status::Ok);
613
+
614
+ receiver
615
+ .recv()
616
+ .unwrap()
617
+ .expect("Failed call to get_contract_and_function_name")
618
+ }
619
+
620
+ fn format(&self, message: impl ToString) -> String {
621
+ let message = message.to_string();
622
+
623
+ if message.is_empty() {
624
+ message
625
+ } else {
626
+ message
627
+ .split('\n')
628
+ .map(|line| format!("{:indent$}{line}", "", indent = self.indentation))
629
+ .collect::<Vec<_>>()
630
+ .join("\n")
631
+ }
632
+ }
633
+
634
+ fn indented(&mut self, display_fn: impl FnOnce(&mut Self)) {
635
+ self.indentation += 2;
636
+ display_fn(self);
637
+ self.indentation -= 2;
638
+ }
639
+
640
+ fn try_indented(
641
+ &mut self,
642
+ display_fn: impl FnOnce(&mut Self) -> Result<(), LoggerError>,
643
+ ) -> Result<(), LoggerError> {
644
+ self.indentation += 2;
645
+ let result = display_fn(self);
646
+ self.indentation -= 2;
647
+
648
+ result
649
+ }
650
+
651
+ fn log(&mut self, message: impl ToString) {
652
+ let formatted = self.format(message);
653
+
654
+ self.logs.push(LogLine::Single(formatted));
655
+ }
656
+
657
+ fn log_auto_mined_block_results(
658
+ &mut self,
659
+ spec_id: edr_eth::SpecId,
660
+ results: &[edr_provider::DebugMineBlockResult<BlockchainError>],
661
+ sent_transaction_hash: &B256,
662
+ ) {
663
+ for result in results {
664
+ self.log_block_from_auto_mine(spec_id, result, sent_transaction_hash);
665
+ }
666
+ }
667
+
668
+ fn log_base_fee(&mut self, base_fee: Option<&U256>) {
669
+ if let Some(base_fee) = base_fee {
670
+ self.log(format!("Base fee: {base_fee}"));
671
+ }
672
+ }
673
+
674
+ fn log_block_from_auto_mine(
675
+ &mut self,
676
+ spec_id: edr_eth::SpecId,
677
+ result: &edr_provider::DebugMineBlockResult<BlockchainError>,
678
+ transaction_hash_to_highlight: &edr_eth::B256,
679
+ ) {
680
+ let edr_provider::DebugMineBlockResult {
681
+ block,
682
+ transaction_results,
683
+ transaction_traces,
684
+ console_log_inputs,
685
+ } = result;
686
+
687
+ let transactions = block.transactions();
688
+ let num_transactions = transactions.len();
689
+
690
+ debug_assert_eq!(num_transactions, transaction_results.len());
691
+ debug_assert_eq!(num_transactions, transaction_traces.len());
692
+
693
+ let block_header = block.header();
694
+
695
+ self.indented(|logger| {
696
+ logger.log_block_id(block);
697
+
698
+ logger.indented(|logger| {
699
+ logger.log_base_fee(block_header.base_fee_per_gas.as_ref());
700
+
701
+ for (idx, transaction, result, trace) in izip!(
702
+ 0..num_transactions,
703
+ transactions,
704
+ transaction_results,
705
+ transaction_traces
706
+ ) {
707
+ let should_highlight_hash =
708
+ *transaction.hash() == *transaction_hash_to_highlight;
709
+ logger.log_block_transaction(
710
+ spec_id,
711
+ transaction,
712
+ result,
713
+ trace,
714
+ console_log_inputs,
715
+ should_highlight_hash,
716
+ );
717
+
718
+ logger.log_empty_line_between_transactions(idx, num_transactions);
719
+ }
720
+ });
721
+ });
722
+
723
+ self.log_empty_line();
724
+ }
725
+
726
+ fn log_block_hash(&mut self, block: &dyn SyncBlock<Error = BlockchainError>) {
727
+ let block_hash = block.hash();
728
+
729
+ self.log(format!("Block: {block_hash}"));
730
+ }
731
+
732
+ fn log_block_id(&mut self, block: &dyn SyncBlock<Error = BlockchainError>) {
733
+ let block_number = block.header().number;
734
+ let block_hash = block.hash();
735
+
736
+ self.log(format!("Block #{block_number}: {block_hash}"));
737
+ }
738
+
739
+ fn log_block_number(&mut self, block: &dyn SyncBlock<Error = BlockchainError>) {
740
+ let block_number = block.header().number;
741
+
742
+ self.log(format!("Mined block #{block_number}"));
743
+ }
744
+
745
+ /// Logs a transaction that's part of a block.
746
+ fn log_block_transaction(
747
+ &mut self,
748
+ spec_id: edr_eth::SpecId,
749
+ transaction: &edr_evm::ExecutableTransaction,
750
+ result: &edr_evm::ExecutionResult,
751
+ trace: &edr_evm::trace::Trace,
752
+ console_log_inputs: &[Bytes],
753
+ should_highlight_hash: bool,
754
+ ) {
755
+ let transaction_hash = transaction.hash();
756
+ if should_highlight_hash {
757
+ self.log_with_title(
758
+ "Transaction",
759
+ Style::new().bold().paint(transaction_hash.to_string()),
760
+ );
761
+ } else {
762
+ self.log_with_title("Transaction", transaction_hash.to_string());
763
+ }
764
+
765
+ self.indented(|logger| {
766
+ logger.log_contract_and_function_name::<false>(spec_id, trace);
767
+ logger.log_with_title("From", format!("0x{:x}", transaction.caller()));
768
+ if let Some(to) = transaction.to() {
769
+ logger.log_with_title("To", format!("0x{to:x}"));
770
+ }
771
+ logger.log_with_title("Value", wei_to_human_readable(transaction.value()));
772
+ logger.log_with_title(
773
+ "Gas used",
774
+ format!(
775
+ "{gas_used} of {gas_limit}",
776
+ gas_used = result.gas_used(),
777
+ gas_limit = transaction.gas_limit()
778
+ ),
779
+ );
780
+
781
+ logger.log_console_log_messages(console_log_inputs);
782
+
783
+ let transaction_failure = edr_provider::TransactionFailure::from_execution_result(
784
+ result,
785
+ transaction_hash,
786
+ trace,
787
+ );
788
+
789
+ if let Some(transaction_failure) = transaction_failure {
790
+ logger.log_transaction_failure(&transaction_failure);
791
+ }
792
+ });
793
+ }
794
+
795
+ fn log_console_log_messages(&mut self, console_log_inputs: &[Bytes]) {
796
+ let (sender, receiver) = channel();
797
+
798
+ let status = self.decode_console_log_inputs_fn.call(
799
+ DecodeConsoleLogInputsCall {
800
+ inputs: console_log_inputs.to_vec(),
801
+ sender,
802
+ },
803
+ ThreadsafeFunctionCallMode::Blocking,
804
+ );
805
+ assert_eq!(status, Status::Ok);
806
+
807
+ let console_log_inputs = receiver
808
+ .recv()
809
+ .unwrap()
810
+ .expect("Failed call to decode_console_log_inputs");
811
+ // This is a special case, as we always want to print the console.log messages.
812
+ // The difference is how. If we have a logger, we should use that, so that logs
813
+ // are printed in order. If we don't, we just print the messages here.
814
+ if self.is_enabled {
815
+ if !console_log_inputs.is_empty() {
816
+ self.log_empty_line();
817
+ self.log("console.log:");
818
+
819
+ self.indented(|logger| {
820
+ for input in console_log_inputs {
821
+ logger.log(input);
822
+ }
823
+ });
824
+ }
825
+ } else {
826
+ for input in console_log_inputs {
827
+ let status = self
828
+ .print_line_fn
829
+ .call((input, false), ThreadsafeFunctionCallMode::Blocking);
830
+
831
+ assert_eq!(status, napi::Status::Ok);
832
+ }
833
+ }
834
+ }
835
+
836
+ fn log_contract_and_function_name<const PRINT_INVALID_CONTRACT_WARNING: bool>(
837
+ &mut self,
838
+ spec_id: edr_eth::SpecId,
839
+ trace: &edr_evm::trace::Trace,
840
+ ) {
841
+ if let Some(TraceMessage::Before(before_message)) = trace.messages.first() {
842
+ if let Some(to) = before_message.to {
843
+ // Call
844
+ let is_precompile = {
845
+ let num_precompiles =
846
+ Precompiles::new(precompile::SpecId::from_spec_id(spec_id)).len();
847
+ precompile::is_precompile(to, num_precompiles)
848
+ };
849
+
850
+ if is_precompile {
851
+ let precompile = u16::from_be_bytes([to[18], to[19]]);
852
+ self.log_with_title(
853
+ "Precompile call",
854
+ format!("<PrecompileContract {precompile}>"),
855
+ );
856
+ } else {
857
+ let is_code_empty = before_message
858
+ .code
859
+ .as_ref()
860
+ .map_or(true, edr_evm::Bytecode::is_empty);
861
+
862
+ if is_code_empty {
863
+ if PRINT_INVALID_CONTRACT_WARNING {
864
+ self.log("WARNING: Calling an account which is not a contract");
865
+ }
866
+ } else {
867
+ let (contract_name, function_name) = self.contract_and_function_name(
868
+ before_message
869
+ .code
870
+ .as_ref()
871
+ .map(edr_evm::Bytecode::original_bytes)
872
+ .expect("Call must be defined"),
873
+ Some(before_message.data.clone()),
874
+ );
875
+
876
+ let function_name = function_name.expect("Function name must be defined");
877
+ self.log_with_title(
878
+ "Contract call",
879
+ if function_name.is_empty() {
880
+ contract_name
881
+ } else {
882
+ format!("{contract_name}#{function_name}")
883
+ },
884
+ );
885
+ }
886
+ }
887
+ } else {
888
+ let result = if let Some(TraceMessage::After(result)) = trace.messages.last() {
889
+ result
890
+ } else {
891
+ unreachable!("Before messages must have an after message")
892
+ };
893
+
894
+ // Create
895
+ let (contract_name, _) =
896
+ self.contract_and_function_name(before_message.data.clone(), None);
897
+
898
+ self.log_with_title("Contract deployment", contract_name);
899
+
900
+ if let ExecutionResult::Success { output, .. } = result {
901
+ if let edr_evm::Output::Create(_, address) = output {
902
+ if let Some(deployed_address) = address {
903
+ self.log_with_title(
904
+ "Contract address",
905
+ format!("0x{deployed_address:x}"),
906
+ );
907
+ }
908
+ } else {
909
+ unreachable!("Create calls must return a Create output")
910
+ }
911
+ }
912
+ }
913
+ }
914
+ }
915
+
916
+ fn log_empty_block(&mut self, block: &dyn SyncBlock<Error = BlockchainError>) {
917
+ let block_header = block.header();
918
+ let block_number = block_header.number;
919
+
920
+ let base_fee = if let Some(base_fee) = block_header.base_fee_per_gas.as_ref() {
921
+ format!(" with base fee {base_fee}")
922
+ } else {
923
+ String::new()
924
+ };
925
+
926
+ self.log(format!("Mined empty block #{block_number}{base_fee}",));
927
+ }
928
+
929
+ fn log_empty_line(&mut self) {
930
+ self.log("");
931
+ }
932
+
933
+ fn log_empty_line_between_transactions(&mut self, idx: usize, num_transactions: usize) {
934
+ if num_transactions > 1 && idx < num_transactions - 1 {
935
+ self.log_empty_line();
936
+ }
937
+ }
938
+
939
+ fn log_hardhat_mined_empty_block(
940
+ &mut self,
941
+ block: &dyn SyncBlock<Error = BlockchainError>,
942
+ empty_blocks_range_start: Option<u64>,
943
+ ) {
944
+ self.indented(|logger| {
945
+ if let Some(empty_blocks_range_start) = empty_blocks_range_start {
946
+ logger.replace_last_log_line(format!(
947
+ "Mined empty block range #{empty_blocks_range_start} to #{block_number}",
948
+ block_number = block.header().number
949
+ ));
950
+ } else {
951
+ logger.log_empty_block(block);
952
+ }
953
+ });
954
+ }
955
+
956
+ /// Logs the result of interval mining a block.
957
+ fn log_interval_mined_block(
958
+ &mut self,
959
+ spec_id: edr_eth::SpecId,
960
+ result: &edr_provider::DebugMineBlockResult<BlockchainError>,
961
+ ) {
962
+ let edr_provider::DebugMineBlockResult {
963
+ block,
964
+ transaction_results,
965
+ transaction_traces,
966
+ console_log_inputs,
967
+ } = result;
968
+
969
+ let transactions = block.transactions();
970
+ let num_transactions = transactions.len();
971
+
972
+ debug_assert_eq!(num_transactions, transaction_results.len());
973
+ debug_assert_eq!(num_transactions, transaction_traces.len());
974
+
975
+ let block_header = block.header();
976
+
977
+ self.indented(|logger| {
978
+ logger.log_block_hash(block);
979
+
980
+ logger.indented(|logger| {
981
+ logger.log_base_fee(block_header.base_fee_per_gas.as_ref());
982
+
983
+ for (idx, transaction, result, trace) in izip!(
984
+ 0..num_transactions,
985
+ transactions,
986
+ transaction_results,
987
+ transaction_traces
988
+ ) {
989
+ logger.log_block_transaction(
990
+ spec_id,
991
+ transaction,
992
+ result,
993
+ trace,
994
+ console_log_inputs,
995
+ false,
996
+ );
997
+
998
+ logger.log_empty_line_between_transactions(idx, num_transactions);
999
+ }
1000
+ });
1001
+ });
1002
+ }
1003
+
1004
+ fn log_hardhat_mined_block(
1005
+ &mut self,
1006
+ spec_id: edr_eth::SpecId,
1007
+ result: &edr_provider::DebugMineBlockResult<BlockchainError>,
1008
+ ) {
1009
+ let edr_provider::DebugMineBlockResult {
1010
+ block,
1011
+ transaction_results,
1012
+ transaction_traces,
1013
+ console_log_inputs,
1014
+ } = result;
1015
+
1016
+ let transactions = block.transactions();
1017
+ let num_transactions = transactions.len();
1018
+
1019
+ debug_assert_eq!(num_transactions, transaction_results.len());
1020
+ debug_assert_eq!(num_transactions, transaction_traces.len());
1021
+
1022
+ self.indented(|logger| {
1023
+ if transactions.is_empty() {
1024
+ logger.log_empty_block(block);
1025
+ } else {
1026
+ logger.log_block_number(block);
1027
+
1028
+ logger.indented(|logger| {
1029
+ logger.log_block_hash(block);
1030
+
1031
+ logger.indented(|logger| {
1032
+ logger.log_base_fee(block.header().base_fee_per_gas.as_ref());
1033
+
1034
+ for (idx, transaction, result, trace) in izip!(
1035
+ 0..num_transactions,
1036
+ transactions,
1037
+ transaction_results,
1038
+ transaction_traces
1039
+ ) {
1040
+ logger.log_block_transaction(
1041
+ spec_id,
1042
+ transaction,
1043
+ result,
1044
+ trace,
1045
+ console_log_inputs,
1046
+ false,
1047
+ );
1048
+
1049
+ logger.log_empty_line_between_transactions(idx, num_transactions);
1050
+ }
1051
+ });
1052
+ });
1053
+ }
1054
+ });
1055
+ }
1056
+
1057
+ /// Logs a warning about multiple blocks being mined.
1058
+ fn log_multiple_blocks_warning(&mut self) {
1059
+ self.indented(|logger| {
1060
+ logger
1061
+ .log("There were other pending transactions. More than one block had to be mined:");
1062
+ });
1063
+ self.log_empty_line();
1064
+ }
1065
+
1066
+ /// Logs a warning about multiple transactions being mined.
1067
+ fn log_multiple_transactions_warning(&mut self) {
1068
+ self.indented(|logger| {
1069
+ logger.log("There were other pending transactions mined in the same block:");
1070
+ });
1071
+ self.log_empty_line();
1072
+ }
1073
+
1074
+ fn log_with_title(&mut self, title: impl Into<String>, message: impl Display) {
1075
+ // repeat whitespace self.indentation times and concatenate with title
1076
+ let title = format!("{:indent$}{}", "", title.into(), indent = self.indentation);
1077
+ if title.len() > self.title_length {
1078
+ self.title_length = title.len();
1079
+ }
1080
+
1081
+ let message = format!("{message}");
1082
+ self.logs.push(LogLine::WithTitle(title, message));
1083
+ }
1084
+
1085
+ fn log_currently_sent_transaction(
1086
+ &mut self,
1087
+ spec_id: edr_eth::SpecId,
1088
+ block_result: &edr_provider::DebugMineBlockResult<BlockchainError>,
1089
+ transaction: &ExecutableTransaction,
1090
+ transaction_result: &edr_evm::ExecutionResult,
1091
+ trace: &edr_evm::trace::Trace,
1092
+ ) {
1093
+ self.indented(|logger| {
1094
+ logger.log("Currently sent transaction:");
1095
+ logger.log("");
1096
+ });
1097
+
1098
+ self.log_transaction(
1099
+ spec_id,
1100
+ block_result,
1101
+ transaction,
1102
+ transaction_result,
1103
+ trace,
1104
+ );
1105
+ }
1106
+
1107
+ fn log_single_transaction_mining_result(
1108
+ &mut self,
1109
+ spec_id: edr_eth::SpecId,
1110
+ result: &edr_provider::DebugMineBlockResult<BlockchainError>,
1111
+ transaction: &ExecutableTransaction,
1112
+ ) {
1113
+ let trace = result
1114
+ .transaction_traces
1115
+ .first()
1116
+ .expect("A transaction exists, so the trace must exist as well.");
1117
+
1118
+ let transaction_result = result
1119
+ .transaction_results
1120
+ .first()
1121
+ .expect("A transaction exists, so the result must exist as well.");
1122
+
1123
+ self.log_transaction(spec_id, result, transaction, transaction_result, trace);
1124
+ }
1125
+
1126
+ fn log_transaction(
1127
+ &mut self,
1128
+ spec_id: edr_eth::SpecId,
1129
+ block_result: &edr_provider::DebugMineBlockResult<BlockchainError>,
1130
+ transaction: &ExecutableTransaction,
1131
+ transaction_result: &edr_evm::ExecutionResult,
1132
+ trace: &edr_evm::trace::Trace,
1133
+ ) {
1134
+ self.indented(|logger| {
1135
+ logger.log_contract_and_function_name::<false>(spec_id, trace);
1136
+
1137
+ let transaction_hash = transaction.hash();
1138
+ logger.log_with_title("Transaction", transaction_hash);
1139
+
1140
+ logger.log_with_title("From", format!("0x{:x}", transaction.caller()));
1141
+ if let Some(to) = transaction.to() {
1142
+ logger.log_with_title("To", format!("0x{to:x}"));
1143
+ }
1144
+ logger.log_with_title("Value", wei_to_human_readable(transaction.value()));
1145
+ logger.log_with_title(
1146
+ "Gas used",
1147
+ format!(
1148
+ "{gas_used} of {gas_limit}",
1149
+ gas_used = transaction_result.gas_used(),
1150
+ gas_limit = transaction.gas_limit()
1151
+ ),
1152
+ );
1153
+
1154
+ let block_number = block_result.block.header().number;
1155
+ logger.log_with_title(format!("Block #{block_number}"), block_result.block.hash());
1156
+
1157
+ logger.log_console_log_messages(&block_result.console_log_inputs);
1158
+
1159
+ let transaction_failure = edr_provider::TransactionFailure::from_execution_result(
1160
+ transaction_result,
1161
+ transaction_hash,
1162
+ trace,
1163
+ );
1164
+
1165
+ if let Some(transaction_failure) = transaction_failure {
1166
+ logger.log_transaction_failure(&transaction_failure);
1167
+ }
1168
+ });
1169
+ }
1170
+
1171
+ fn print<const REPLACE: bool>(&mut self, message: impl ToString) -> Result<(), LoggerError> {
1172
+ if !self.is_enabled {
1173
+ return Ok(());
1174
+ }
1175
+
1176
+ let formatted = self.format(message);
1177
+
1178
+ let status = self
1179
+ .print_line_fn
1180
+ .call((formatted, REPLACE), ThreadsafeFunctionCallMode::Blocking);
1181
+
1182
+ if status == napi::Status::Ok {
1183
+ Ok(())
1184
+ } else {
1185
+ Err(LoggerError::PrintLine)
1186
+ }
1187
+ }
1188
+
1189
+ fn print_empty_line(&mut self) -> Result<(), LoggerError> {
1190
+ self.print::<false>("")
1191
+ }
1192
+
1193
+ fn print_logs(&mut self) -> Result<bool, LoggerError> {
1194
+ let logs = std::mem::take(&mut self.logs);
1195
+ if logs.is_empty() {
1196
+ return Ok(false);
1197
+ }
1198
+
1199
+ for log in logs {
1200
+ let line = match log {
1201
+ LogLine::Single(message) => message,
1202
+ LogLine::WithTitle(title, message) => {
1203
+ let title = format!("{title}:");
1204
+ format!("{title:indent$} {message}", indent = self.title_length + 1)
1205
+ }
1206
+ };
1207
+
1208
+ self.print::<false>(line)?;
1209
+ }
1210
+
1211
+ Ok(true)
1212
+ }
1213
+
1214
+ fn print_method(&mut self, method: &str) -> Result<(), LoggerError> {
1215
+ if let Some(collapsed_method) = self.collapsed_method(method) {
1216
+ collapsed_method.count += 1;
1217
+
1218
+ let line = format!("{method} ({count})", count = collapsed_method.count);
1219
+ self.print::<true>(Color::Green.paint(line))
1220
+ } else {
1221
+ self.state = LoggingState::CollapsingMethod(CollapsedMethod {
1222
+ count: 1,
1223
+ method: method.to_string(),
1224
+ });
1225
+
1226
+ self.print::<false>(Color::Green.paint(method))
1227
+ }
1228
+ }
1229
+
1230
+ /// Retrieves the collapsed method with the provided name, if it exists.
1231
+ fn collapsed_method(&mut self, method: &str) -> Option<&mut CollapsedMethod> {
1232
+ if let LoggingState::CollapsingMethod(collapsed_method) = &mut self.state {
1233
+ if collapsed_method.method == method {
1234
+ return Some(collapsed_method);
1235
+ }
1236
+ }
1237
+
1238
+ None
1239
+ }
1240
+
1241
+ fn replace_last_log_line(&mut self, message: impl ToString) {
1242
+ let formatted = self.format(message);
1243
+
1244
+ *self.logs.last_mut().expect("There must be a log line") = LogLine::Single(formatted);
1245
+ }
1246
+ }
1247
+
1248
+ fn wei_to_human_readable(wei: U256) -> String {
1249
+ if wei == U256::ZERO {
1250
+ "0 ETH".to_string()
1251
+ } else if wei < U256::from(100_000u64) {
1252
+ format!("{wei} wei")
1253
+ } else if wei < U256::from(100_000_000_000_000u64) {
1254
+ let mut decimal = to_decimal_string(wei, 9);
1255
+ decimal.push_str(" gwei");
1256
+ decimal
1257
+ } else {
1258
+ let mut decimal = to_decimal_string(wei, 18);
1259
+ decimal.push_str(" ETH");
1260
+ decimal
1261
+ }
1262
+ }
1263
+
1264
+ /// Converts the provided `value` to a decimal string after dividing it by
1265
+ /// `10^exponent`. The returned string will have at most `MAX_DECIMALS`
1266
+ /// decimals.
1267
+ fn to_decimal_string(value: U256, exponent: u8) -> String {
1268
+ const MAX_DECIMALS: u8 = 4;
1269
+
1270
+ let (integer, remainder) = value.div_rem(U256::from(10).pow(U256::from(exponent)));
1271
+ let decimal = remainder / U256::from(10).pow(U256::from(exponent - MAX_DECIMALS));
1272
+
1273
+ // Remove trailing zeros
1274
+ let decimal = decimal.to_string().trim_end_matches('0').to_string();
1275
+
1276
+ format!("{integer}.{decimal}")
1277
+ }