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