referrerpolicy=no-referrer-when-downgrade

pallet_revive_eth_rpc/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17//! The [`EthRpcServer`] RPC server implementation
18#![cfg_attr(docsrs, feature(doc_cfg))]
19
20pub use alloy_rpc_types::{BlockId, BlockNumberOrTag, Filter, FilterBlockOption};
21use client::ClientError;
22use futures::{Stream, StreamExt, TryStreamExt};
23use jsonrpsee::{
24	PendingSubscriptionSink, SubscriptionMessage, SubscriptionSink,
25	core::{RpcResult, async_trait},
26	types::{ErrorCode, ErrorObjectOwned},
27};
28use pallet_revive::evm::*;
29use pallet_revive_types::runtime_api::{
30	BlockV1, ExecutionTracerConfigV1, GenericTransactionV1, ReceiptGasInfoV1, StateOverrideSetV1,
31	TraceV1, TracerTypeV1, TransactionInfoV1,
32};
33use sp_core::{H160, H256, U256};
34use sp_crypto_hashing::keccak_256;
35use std::pin::Pin;
36use subxt::rpcs::methods::legacy::TransactionStatus;
37use thiserror::Error;
38use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError};
39
40mod block_sync;
41pub(crate) use block_sync::{ChainMetadata, SyncLabel, SyncStateKey};
42pub mod cli;
43pub mod client;
44pub mod example;
45pub mod subxt_client;
46
47#[cfg(test)]
48mod tests;
49
50mod block_info_provider;
51pub use block_info_provider::*;
52
53mod receipt_provider;
54pub use receipt_provider::*;
55
56mod fee_history_provider;
57pub use fee_history_provider::*;
58
59mod receipt_extractor;
60pub use receipt_extractor::*;
61
62mod apis;
63pub use apis::*;
64
65mod types;
66pub use types::*;
67
68pub const LOG_TARGET: &str = "eth-rpc";
69
70/// An EVM RPC server implementation.
71pub struct EthRpcServerImpl {
72	/// The client used to interact with the substrate node.
73	client: client::Client,
74
75	/// The accounts managed by the server.
76	accounts: Vec<Account>,
77
78	/// Controls if unprotected txs are allowed or not.
79	allow_unprotected_txs: bool,
80
81	/// When true, estimate_gas uses Pending block if no block is specified.
82	use_pending_for_estimate_gas: bool,
83}
84
85impl EthRpcServerImpl {
86	/// Creates a new [`EthRpcServerImpl`].
87	pub fn new(client: client::Client) -> Self {
88		Self {
89			client,
90			accounts: vec![],
91			allow_unprotected_txs: false,
92			use_pending_for_estimate_gas: false,
93		}
94	}
95
96	/// Sets the accounts managed by the server.
97	pub fn with_accounts(mut self, accounts: Vec<Account>) -> Self {
98		self.accounts = accounts;
99		self
100	}
101
102	/// Sets whether unprotected transactions are allowed or not.
103	pub fn with_allow_unprotected_txs(mut self, allow_unprotected_txs: bool) -> Self {
104		self.allow_unprotected_txs = allow_unprotected_txs;
105		self
106	}
107
108	/// Sets whether estimate_gas uses Pending block when no block is specified.
109	pub fn with_use_pending_for_estimate_gas(mut self, use_pending_for_estimate_gas: bool) -> Self {
110		self.use_pending_for_estimate_gas = use_pending_for_estimate_gas;
111		self
112	}
113}
114
115/// The error type for the EVM RPC server.
116#[derive(Error, Debug)]
117pub enum EthRpcError {
118	/// A [`ClientError`] wrapper error.
119	#[error("Client error: {0}")]
120	ClientError(#[from] ClientError),
121	/// A [`rlp::DecoderError`] wrapper error.
122	#[error("Decoding error: {0}")]
123	RlpError(#[from] rlp::DecoderError),
124	/// A Decimals conversion error.
125	#[error("Conversion error")]
126	ConversionError,
127	/// An invalid signature error.
128	#[error("Invalid signature")]
129	InvalidSignature,
130	/// The account was not found at the given address
131	#[error("Account not found for address {0:?}")]
132	AccountNotFound(H160),
133	/// Received an invalid transaction
134	#[error("Invalid transaction")]
135	InvalidTransaction,
136	/// Received an invalid transaction
137	#[error("Invalid transaction {0:?}")]
138	TransactionTypeNotSupported(Byte),
139	/// The requested `eth_feeHistory` reward percentiles are invalid.
140	#[error("{0}")]
141	InvalidRewardPercentiles(String),
142}
143
144impl From<EthRpcError> for ErrorObjectOwned {
145	fn from(value: EthRpcError) -> Self {
146		use jsonrpsee::types::error::CALL_EXECUTION_FAILED_CODE;
147		let message = value.to_string();
148		let code = match value {
149			// `ClientError` already produces a fully formed JSON-RPC error object.
150			EthRpcError::ClientError(err) => return Self::from(err),
151			EthRpcError::ConversionError => ErrorCode::InvalidParams.code(),
152			// Matches Geth/Nethermind, which return `-32000` for these execution-time errors.
153			EthRpcError::RlpError(_) |
154			EthRpcError::InvalidSignature |
155			EthRpcError::AccountNotFound(_) |
156			EthRpcError::InvalidTransaction |
157			EthRpcError::InvalidRewardPercentiles(_) |
158			EthRpcError::TransactionTypeNotSupported(_) => CALL_EXECUTION_FAILED_CODE,
159		};
160		Self::owned::<String>(code, message, None)
161	}
162}
163
164#[async_trait]
165impl EthRpcServer for EthRpcServerImpl {
166	async fn net_version(&self) -> RpcResult<String> {
167		Ok(self.client.chain_id().to_string())
168	}
169
170	async fn net_listening(&self) -> RpcResult<bool> {
171		let syncing = self.client.syncing().await?;
172		let listening = matches!(syncing, SyncingStatus::Bool(false));
173		Ok(listening)
174	}
175
176	async fn syncing(&self) -> RpcResult<SyncingStatus> {
177		Ok(self.client.syncing().await?)
178	}
179
180	async fn block_number(&self) -> RpcResult<U256> {
181		let number = self.client.block_number().await?;
182		Ok(number.into())
183	}
184
185	async fn get_transaction_receipt(
186		&self,
187		transaction_hash: H256,
188	) -> RpcResult<Option<ReceiptInfo>> {
189		let receipt = self.client.receipt(&transaction_hash).await;
190		Ok(receipt)
191	}
192
193	/// Performs gas estimations to find the lowest gas limit required to run the transaction.
194	///
195	/// This method implements the same gas estimation logic found in Geth which performs binary
196	/// search with some simple heuristics to find the smallest gas limit for the transaction.
197	async fn estimate_gas(
198		&self,
199		transaction: GenericTransactionV1,
200		block: Option<BlockNumberOrTag>,
201	) -> RpcResult<U256> {
202		log::trace!(target: LOG_TARGET, "estimate_gas transaction={transaction:?} block={block:?}");
203
204		let block = block.unwrap_or_else(|| {
205			if self.use_pending_for_estimate_gas {
206				BlockNumberOrTag::Pending
207			} else {
208				Default::default()
209			}
210		});
211		let block = BlockId::from(block);
212		let hash = self.client.block_hash_for_tag(block).await?;
213		let gas_estimate =
214			self.client.runtime_api(hash).await?.estimate_gas(transaction, block).await?;
215
216		log::trace!(
217			target: LOG_TARGET,
218			"estimate_gas result={gas_estimate:?}",
219		);
220		Ok(gas_estimate)
221	}
222
223	async fn call(
224		&self,
225		transaction: GenericTransactionV1,
226		block: Option<BlockId>,
227		state_overrides: Option<StateOverrideSetV1>,
228	) -> RpcResult<Bytes> {
229		let block = block.unwrap_or_default();
230		let hash = self.client.block_hash_for_tag(block).await?;
231		let runtime_api = self.client.runtime_api(hash).await?;
232		let dry_run = runtime_api.dry_run(transaction, block, state_overrides).await?;
233		Ok(dry_run.data.into())
234	}
235
236	async fn send_raw_transaction(&self, transaction: Bytes) -> RpcResult<H256> {
237		let hash = H256(keccak_256(&transaction.0));
238		log::trace!(target: LOG_TARGET, "send_raw_transaction transaction: {transaction:?} ethereum_hash: {hash:?}");
239
240		if !self.allow_unprotected_txs {
241			let signed_transaction = TransactionSigned::decode(transaction.0.as_slice())
242				.map_err(|err| {
243					log::trace!(target: LOG_TARGET, "Transaction decoding failed. ethereum_hash: {hash:?}, error: {err:?}");
244					EthRpcError::InvalidTransaction
245				})?;
246
247			let is_chain_id_provided = match signed_transaction {
248				TransactionSigned::Transaction7702Signed(tx) => {
249					tx.transaction_7702_unsigned.chain_id != U256::zero()
250				},
251				TransactionSigned::Transaction4844Signed(tx) => {
252					tx.transaction_4844_unsigned.chain_id != U256::zero()
253				},
254				TransactionSigned::Transaction1559Signed(tx) => {
255					tx.transaction_1559_unsigned.chain_id != U256::zero()
256				},
257				TransactionSigned::Transaction2930Signed(tx) => {
258					tx.transaction_2930_unsigned.chain_id != U256::zero()
259				},
260				TransactionSigned::TransactionLegacySigned(tx) => {
261					tx.transaction_legacy_unsigned.chain_id.is_some()
262				},
263			};
264
265			if !is_chain_id_provided {
266				log::trace!(target: LOG_TARGET, "Invalid Transaction: transaction doesn't include a chain-id. ethereum_hash: {hash:?}");
267				Err(EthRpcError::InvalidTransaction)?;
268			}
269		}
270
271		let call = subxt_client::tx().revive().eth_transact(transaction.0);
272
273		// Subscribe to new block only when automine is enabled.
274		let receiver = self.client.block_notifier().map(|sender| sender.subscribe());
275
276		// Submit the transaction
277		let tx_status = self.client.submit(call).await.map_err(|err| {
278			log::trace!(target: LOG_TARGET, "send_raw_transaction ethereum_hash: {hash:?} failed: {err:?}");
279			err
280		})?;
281
282		if matches!(tx_status, TransactionStatus::Future) {
283			return Ok(hash);
284		}
285
286		// Wait for the transaction to be included in a block if automine is enabled
287		if let Some(mut receiver) = receiver {
288			loop {
289				if let Ok(block_hash) = receiver.recv().await {
290					let Ok(Some(block)) = self.client.block_by_hash(&block_hash).await else {
291						log::debug!(target: LOG_TARGET, "Could not find the block with the received hash: {hash:?}.");
292						continue;
293					};
294					let Some(evm_block) = self.client.evm_block(block, false).await else {
295						log::debug!(target: LOG_TARGET, "Failed to get the EVM block for substrate block with hash: {hash:?}");
296						continue;
297					};
298					if evm_block.transactions.contains_tx(hash) {
299						log::debug!(target: LOG_TARGET, "{hash:} was included in a block");
300						break;
301					}
302				}
303			}
304		}
305
306		log::debug!(target: LOG_TARGET, "send_raw_transaction hash: {hash:?}");
307		Ok(hash)
308	}
309
310	async fn send_transaction(&self, mut transaction: GenericTransactionV1) -> RpcResult<H256> {
311		log::debug!(target: LOG_TARGET, "{transaction:#?}");
312
313		let Some(from) = transaction.from else {
314			log::debug!(target: LOG_TARGET, "Transaction must have a sender");
315			return Err(EthRpcError::InvalidTransaction.into());
316		};
317
318		let account = self
319			.accounts
320			.iter()
321			.find(|account| account.address() == from)
322			.ok_or(EthRpcError::AccountNotFound(from))?;
323
324		if transaction.gas.is_none() {
325			transaction.gas = Some(self.estimate_gas(transaction.clone(), None).await?);
326		}
327
328		if transaction.gas_price.is_none() {
329			transaction.gas_price = Some(self.gas_price().await?);
330		}
331
332		if transaction.nonce.is_none() {
333			transaction.nonce = Some(self.get_transaction_count(from, Default::default()).await?);
334		}
335
336		if transaction.chain_id.is_none() {
337			transaction.chain_id = Some(self.chain_id().await?);
338		}
339
340		let tx = GenericTransaction::from(transaction)
341			.try_into_unsigned()
342			.map_err(|_| EthRpcError::InvalidTransaction)?;
343		let payload = account.sign_transaction(tx).signed_payload();
344		self.send_raw_transaction(Bytes(payload)).await
345	}
346
347	async fn get_block_by_hash(
348		&self,
349		block_hash: H256,
350		hydrated_transactions: bool,
351	) -> RpcResult<Option<BlockV1>> {
352		let Some(block) = self.client.block_by_ethereum_hash(&block_hash).await? else {
353			return Ok(None);
354		};
355		let block = self.client.evm_block(block, hydrated_transactions).await;
356		Ok(block)
357	}
358
359	async fn get_balance(&self, address: H160, block: BlockId) -> RpcResult<U256> {
360		let hash = self.client.block_hash_for_tag(block).await?;
361		let runtime_api = self.client.runtime_api(hash).await?;
362		let balance = runtime_api.balance(address).await?;
363		Ok(balance)
364	}
365
366	async fn chain_id(&self) -> RpcResult<U256> {
367		Ok(self.client.chain_id().into())
368	}
369
370	async fn gas_price(&self) -> RpcResult<U256> {
371		let hash = self.client.block_hash_for_tag(Default::default()).await?;
372		let runtime_api = self.client.runtime_api(hash).await?;
373		Ok(runtime_api.gas_price().await?)
374	}
375
376	async fn max_priority_fee_per_gas(&self) -> RpcResult<U256> {
377		// We do not support tips. Hence the recommended priority fee is
378		// always zero. The effective gas price will always be the base price.
379		Ok(Default::default())
380	}
381
382	async fn get_code(&self, address: H160, block: BlockId) -> RpcResult<Bytes> {
383		let hash = self.client.block_hash_for_tag(block).await?;
384		let code = self.client.runtime_api(hash).await?.code(address).await?;
385		Ok(code.into())
386	}
387
388	async fn accounts(&self) -> RpcResult<Vec<H160>> {
389		Ok(self.accounts.iter().map(|account| account.address()).collect())
390	}
391
392	async fn get_block_by_number(
393		&self,
394		block_number: BlockNumberOrTag,
395		hydrated_transactions: bool,
396	) -> RpcResult<Option<BlockV1>> {
397		let Some(block) = self.client.block_by_number_or_tag(&block_number).await? else {
398			return Ok(None);
399		};
400		let block = self.client.evm_block(block, hydrated_transactions).await;
401		Ok(block)
402	}
403
404	async fn get_block_receipts(&self, block: BlockId) -> RpcResult<Option<Vec<ReceiptInfo>>> {
405		let receipts = self.client.block_receipts(block).await?;
406		Ok(receipts)
407	}
408
409	async fn get_block_transaction_count_by_hash(
410		&self,
411		block_hash: Option<H256>,
412	) -> RpcResult<Option<U256>> {
413		let block_hash = if let Some(block_hash) = block_hash {
414			block_hash
415		} else {
416			self.client.latest_block().await.hash()
417		};
418
419		let Some(substrate_hash) = self.client.resolve_substrate_hash(&block_hash).await else {
420			return Ok(None);
421		};
422
423		Ok(self.client.receipts_count_per_block(&substrate_hash).await.map(U256::from))
424	}
425
426	async fn get_block_transaction_count_by_number(
427		&self,
428		block: Option<BlockNumberOrTag>,
429	) -> RpcResult<Option<U256>> {
430		let substrate_hash = if let Some(block) = self
431			.client
432			.block_by_number_or_tag(&block.unwrap_or_else(|| BlockNumberOrTag::Latest))
433			.await?
434		{
435			block.block_hash()
436		} else {
437			return Ok(None);
438		};
439
440		Ok(self.client.receipts_count_per_block(&substrate_hash).await.map(U256::from))
441	}
442
443	async fn get_logs(&self, filter: Option<Filter>) -> RpcResult<FilterResults> {
444		let logs = self.client.logs(filter).await?;
445		Ok(FilterResults::Logs(logs))
446	}
447
448	async fn get_storage_at(
449		&self,
450		address: H160,
451		storage_slot: U256,
452		block: BlockId,
453	) -> RpcResult<Bytes> {
454		let hash = self.client.block_hash_for_tag(block).await?;
455		let runtime_api = self.client.runtime_api(hash).await?;
456		let bytes = match runtime_api.get_storage(address, storage_slot.to_big_endian()).await {
457			Ok(value) => value.unwrap_or([0u8; 32].into()),
458			// Per Ethereum spec, return zero for non-contract addresses.
459			Err(ClientError::ContractNotFound) => {
460				log::trace!(target: LOG_TARGET, "get_storage_at: ContractNotFound for {address:?}, returning zero");
461				[0u8; 32].into()
462			},
463			Err(err) => return Err(err.into()),
464		};
465		Ok(bytes.into())
466	}
467
468	async fn get_transaction_by_block_hash_and_index(
469		&self,
470		block_hash: H256,
471		transaction_index: U256,
472	) -> RpcResult<Option<TransactionInfoV1>> {
473		let Some(substrate_block_hash) = self.client.resolve_substrate_hash(&block_hash).await
474		else {
475			return Ok(None);
476		};
477		self.get_transaction_by_substrate_block_hash_and_index(
478			substrate_block_hash,
479			transaction_index,
480		)
481		.await
482	}
483
484	async fn get_transaction_by_block_number_and_index(
485		&self,
486		block: BlockNumberOrTag,
487		transaction_index: U256,
488	) -> RpcResult<Option<TransactionInfoV1>> {
489		let Some(block) = self.client.block_by_number_or_tag(&block).await? else {
490			return Ok(None);
491		};
492		self.get_transaction_by_substrate_block_hash_and_index(
493			block.block_hash(),
494			transaction_index,
495		)
496		.await
497	}
498
499	async fn get_transaction_by_hash(
500		&self,
501		transaction_hash: H256,
502	) -> RpcResult<Option<TransactionInfoV1>> {
503		let receipt = self.client.receipt(&transaction_hash).await;
504		let signed_tx = self.client.signed_tx_by_hash(&transaction_hash).await;
505		if let (Some(receipt), Some(signed_tx)) = (receipt, signed_tx) {
506			return Ok(Some(receipt.transaction_info(signed_tx)));
507		}
508
509		Ok(None)
510	}
511
512	async fn get_transaction_count(&self, address: H160, block: BlockId) -> RpcResult<U256> {
513		let hash = self.client.block_hash_for_tag(block).await?;
514		let runtime_api = self.client.runtime_api(hash).await?;
515		let nonce = runtime_api.nonce(address).await?;
516		Ok(nonce)
517	}
518
519	async fn web3_client_version(&self) -> RpcResult<String> {
520		let git_revision = env!("GIT_REVISION");
521		let rustc_version = env!("RUSTC_VERSION");
522		let target = env!("TARGET");
523		Ok(format!("eth-rpc/{git_revision}/{target}/{rustc_version}"))
524	}
525
526	async fn fee_history(
527		&self,
528		block_count: U256,
529		newest_block: BlockNumberOrTag,
530		reward_percentiles: Option<Vec<f64>>,
531	) -> RpcResult<FeeHistoryResult> {
532		let block_count: u32 = block_count.try_into().map_err(|_| EthRpcError::ConversionError)?;
533
534		// As in go-ethereum, a request for fewer than one block returns an empty result rather
535		// than an error, before any percentile validation.
536		if block_count == 0 {
537			return Ok(FeeHistoryResult {
538				oldest_block: U256::zero(),
539				base_fee_per_gas: vec![],
540				gas_used_ratio: vec![],
541				reward: vec![],
542			});
543		}
544
545		// Reject malformed percentiles up front, as go-ethereum does, instead of silently
546		// clamping or approximating them at the wrong bucket.
547		if let Some(percentiles) = reward_percentiles.as_deref() {
548			validate_reward_percentiles(percentiles)
549				.map_err(EthRpcError::InvalidRewardPercentiles)?;
550		}
551
552		let result = self.client.fee_history(block_count, newest_block, reward_percentiles).await?;
553		Ok(result)
554	}
555
556	async fn eth_subscribe(
557		&self,
558		pending: PendingSubscriptionSink,
559		kind: SubscriptionKind,
560		options: Option<SubscriptionOptions>,
561	) {
562		let Some(subscription_parameters) = SubscriptionParameters::new(kind, options) else {
563			return pending
564				.reject(ErrorObjectOwned::owned(
565					jsonrpsee::types::error::INVALID_PARAMS_CODE,
566					"Invalid subscription parameters",
567					None::<()>,
568				))
569				.await;
570		};
571		let Ok(sink) = pending.accept().await else {
572			return;
573		};
574
575		let stream: Pin<
576			Box<dyn Stream<Item = Result<SubscriptionItem, BroadcastStreamRecvError>> + Send>,
577		> = match subscription_parameters {
578			SubscriptionParameters::NewBlockHeaders => Box::pin(
579				BroadcastStream::new(self.client.get_block_subscription_rx())
580					.map_ok(|block| SubscriptionItem::BlockHeader(BlockHeader::from(block))),
581			) as _,
582			SubscriptionParameters::Logs(filter) => Box::pin(
583				BroadcastStream::new(self.client.get_log_subscription_rx())
584					.try_filter(move |log| futures::future::ready(filter.matches(log)))
585					.map_ok(SubscriptionItem::Log),
586			) as _,
587		};
588		let _ = tokio::spawn(Self::handle_subscription_forwarding(sink, stream));
589	}
590}
591
592impl EthRpcServerImpl {
593	async fn get_transaction_by_substrate_block_hash_and_index(
594		&self,
595		substrate_block_hash: H256,
596		transaction_index: U256,
597	) -> RpcResult<Option<TransactionInfoV1>> {
598		let Some(receipt) = self
599			.client
600			.receipt_by_hash_and_index(
601				&substrate_block_hash,
602				transaction_index.try_into().map_err(|_| EthRpcError::ConversionError)?,
603			)
604			.await
605		else {
606			return Ok(None);
607		};
608		let Some(signed_tx) = self.client.signed_tx_by_hash(&receipt.transaction_hash).await else {
609			return Ok(None);
610		};
611
612		Ok(Some(receipt.transaction_info(signed_tx)))
613	}
614
615	async fn handle_subscription_forwarding(
616		sink: SubscriptionSink,
617		mut stream: Pin<
618			Box<dyn Stream<Item = Result<SubscriptionItem, BroadcastStreamRecvError>> + Send>,
619		>,
620	) {
621		loop {
622			tokio::select! {
623				_ = sink.closed() => break,
624				item = stream.next() => {
625					match item {
626						// Stream ended.
627						None => break,
628						// Send the item to the subscriber.
629						Some(Ok(sub_item)) => {
630							let msg = SubscriptionMessage::from_json(&sub_item)
631								.expect("SubscriptionItem is serializable; qed");
632							if sink.send(msg).await.is_err() {
633								break;
634							}
635						},
636						// Broadcast receiver lagged behind — missed messages.
637						Some(Err(BroadcastStreamRecvError::Lagged(count))) => {
638							log::warn!(
639								target: LOG_TARGET,
640								"Subscription lagged, skipped {count} messages"
641							);
642						},
643					}
644				}
645			}
646		}
647	}
648}
649
650#[cfg(test)]
651mod error_codes_tests {
652	use super::*;
653	use jsonrpsee::types::error::{CALL_EXECUTION_FAILED_CODE, INVALID_PARAMS_CODE};
654
655	#[test]
656	fn eth_rpc_error_maps_to_expected_code_and_message() {
657		let cases: Vec<(EthRpcError, i32)> = vec![
658			(EthRpcError::RlpError(rlp::DecoderError::RlpIsTooShort), CALL_EXECUTION_FAILED_CODE),
659			(EthRpcError::ConversionError, INVALID_PARAMS_CODE),
660			(EthRpcError::InvalidSignature, CALL_EXECUTION_FAILED_CODE),
661			(EthRpcError::AccountNotFound(H160::repeat_byte(0xab)), CALL_EXECUTION_FAILED_CODE),
662			(EthRpcError::InvalidTransaction, CALL_EXECUTION_FAILED_CODE),
663			(
664				EthRpcError::TransactionTypeNotSupported(Byte::from(0x7eu8)),
665				CALL_EXECUTION_FAILED_CODE,
666			),
667		];
668
669		for (err, expected_code) in cases {
670			let expected_message = err.to_string();
671			let obj = ErrorObjectOwned::from(err);
672			assert_eq!(obj.code(), expected_code, "unexpected code for `{expected_message}`");
673			assert_eq!(obj.message(), expected_message);
674		}
675	}
676}