referrerpolicy=no-referrer-when-downgrade

pallet_revive_eth_rpc/
client.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 client connects to the source substrate chain
18//! and is used by the rpc server to query and send transactions to the substrate chain.
19
20pub(crate) mod storage_api;
21pub(crate) mod version_aware_runtime_api;
22
23use crate::{
24	BlockId, BlockInfoProvider, BlockNumberOrTag, FeeHistoryProvider, FeeHistoryResult, Filter,
25	Log, ReceiptInfo, ReceiptProvider, SubxtBlockInfoProvider, SyncLabel, SyncingProgress,
26	SyncingStatus, TraceOutcome, TransactionTrace,
27	block_sync::SyncCheckpoint,
28	subxt_client::{self, SrcChainConfig, revive::calls::EthTransact},
29};
30use futures::TryStreamExt;
31use jsonrpsee::types::{ErrorObjectOwned, error::CALL_EXECUTION_FAILED_CODE};
32use pallet_revive::{
33	EthTransactError,
34	evm::{H256, TransactionSigned, U256, decode_revert_reason},
35};
36use pallet_revive_types::runtime_api::*;
37use sp_runtime::traits::Block as BlockT;
38use sp_weights::Weight;
39use std::{
40	sync::{
41		Arc,
42		atomic::{AtomicBool, AtomicUsize, Ordering},
43	},
44	time::Duration,
45};
46use storage_api::StorageApi;
47use subxt::{
48	OnlineClient,
49	backend::{LegacyBackend, StreamOf, StreamOfResults},
50	client::OnlineClientAtBlock,
51	config::{HashFor, RpcConfigFor},
52	rpcs::{
53		RpcClient,
54		client::reconnecting_rpc_client::{ExponentialBackoff, RpcClient as ReconnectingRpcClient},
55		methods::{
56			LegacyRpcMethods,
57			legacy::{SystemHealth, TransactionStatus},
58		},
59		rpc_params,
60	},
61};
62
63use thiserror::Error;
64use tokio::sync::{Mutex, mpsc};
65use version_aware_runtime_api::{
66	CallRecordedOutput, TraceEntry, VersionAwareRuntimeApi, VersionAwareRuntimeApiProvider,
67};
68
69/// The substrate block number type.
70pub type SubstrateBlockNumber = u64;
71
72/// The substrate block hash type.
73pub type SubstrateBlockHash = HashFor<SrcChainConfig>;
74
75/// A handle to a Substrate block at a specific height.
76pub type SubstrateBlock = OnlineClientAtBlock<SrcChainConfig>;
77
78/// The runtime balance type.
79pub type Balance = u128;
80
81/// The subscription type used to listen to new blocks.
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub enum SubscriptionType {
84	/// Subscribe to best blocks.
85	BestBlocks,
86	/// Subscribe to finalized blocks.
87	FinalizedBlocks,
88}
89
90/// Submit Error reason.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
92pub enum SubmitError {
93	/// Transaction was usurped by another with the same nonce.
94	#[error("Transaction was usurped by another with the same nonce")]
95	Usurped,
96	/// Transaction was dropped from the pool.
97	#[error("Transaction was dropped")]
98	Dropped,
99	/// Transaction is invalid (e.g. bad nonce, signature, etc).
100	#[error("Transaction is invalid (e.g. bad nonce, signature, etc)")]
101	Invalid,
102	/// Transaction stream ended without a terminal status.
103	#[error("Transaction stream ended without status")]
104	StreamEnded,
105	/// Unknown transaction status.
106	#[error("Unknown transaction status")]
107	Unknown,
108}
109
110impl From<TransactionStatus<SubstrateBlockHash>> for SubmitError {
111	fn from(status: TransactionStatus<SubstrateBlockHash>) -> Self {
112		match status {
113			TransactionStatus::Usurped(_) => SubmitError::Usurped,
114			TransactionStatus::Dropped => SubmitError::Dropped,
115			TransactionStatus::Invalid => SubmitError::Invalid,
116			_ => SubmitError::Unknown,
117		}
118	}
119}
120
121/// The error type for the client.
122#[derive(Error, Debug)]
123pub enum ClientError {
124	/// A [`jsonrpsee::core::ClientError`] wrapper error.
125	#[error(transparent)]
126	Jsonrpsee(#[from] jsonrpsee::core::ClientError),
127	/// A [`subxt::Error`] wrapper error.
128	#[error(transparent)]
129	SubxtError(#[from] subxt::Error),
130	#[error(transparent)]
131	RpcError(#[from] subxt::rpcs::Error),
132	/// A [`sqlx::Error`] wrapper error.
133	#[error(transparent)]
134	SqlxError(#[from] sqlx::Error),
135	/// A [`codec::Error`] wrapper error.
136	#[error(transparent)]
137	CodecError(#[from] codec::Error),
138	/// author_submitExtrinsic failed.
139	#[error("Invalid transaction: {0}")]
140	SubmitError(SubmitError),
141	/// Transact call failed.
142	#[error("contract reverted: {0:?}")]
143	TransactError(EthTransactError),
144	/// A decimal conversion failed.
145	#[error("conversion failed")]
146	ConversionFailed,
147	/// The block hash was not found.
148	#[error("hash not found")]
149	BlockNotFound,
150	/// The contract was not found.
151	#[error("Contract not found")]
152	ContractNotFound,
153	#[error("No Ethereum extrinsic found")]
154	EthExtrinsicNotFound,
155	/// The transaction exists but the node could not produce its trace.
156	#[error("trace unavailable: the node could not produce a trace for this transaction")]
157	TraceUnavailable,
158	/// The transaction fee could not be found
159	#[error("transactionFeePaid event not found")]
160	TxFeeNotFound,
161	/// Failed to decode a raw payload into a signed transaction.
162	#[error("Failed to decode a raw payload into a signed transaction")]
163	TxDecodingFailed,
164	/// Failed to recover eth address.
165	#[error("failed to recover eth address")]
166	RecoverEthAddressFailed,
167	/// Failed to filter logs.
168	#[error("Failed to filter logs")]
169	LogFilterFailed(#[from] anyhow::Error),
170	/// Receipt storage was not found.
171	#[error("Receipt storage not found")]
172	ReceiptDataNotFound,
173	/// Ethereum block was not found.
174	#[error("Ethereum block not found")]
175	EthereumBlockNotFound,
176	/// Receipt data length mismatch.
177	#[error("Receipt data length mismatch")]
178	ReceiptDataLengthMismatch,
179	/// Transaction submission timeout.
180	#[error("Transaction submission timeout")]
181	Timeout,
182	/// Chain identity mismatch between stored genesis and connected node.
183	#[error("Genesis hash mismatch")]
184	ChainMismatch,
185	/// Stored sync boundary does not match the connected node.
186	#[error("Sync boundary mismatch")]
187	SyncBoundaryMismatch,
188	/// The runtime of the queried block does not support the requested runtime API method.
189	#[error("The `{0}` runtime API method is not supported at the queried block")]
190	UnsupportedRuntimeApiMethod(&'static str),
191}
192
193impl ClientError {
194	/// Errors that indicate a mismatch between the stored sync state and the connected node.
195	pub(crate) fn is_chain_validation_error(&self) -> bool {
196		matches!(self, Self::ChainMismatch | Self::SyncBoundaryMismatch)
197	}
198
199	/// Why the node cannot service `state_callRecorded`, if it can't; `None` for any genuine
200	/// failure that must propagate rather than fall back to a recorder-less replay.
201	pub(crate) fn recorded_unavailable_reason(&self) -> Option<RecordedUnavailable> {
202		use sc_rpc_api::state::error::{
203			CALL_RECORDED_DENIED_ERROR_CODE, CALL_RECORDED_UNSUPPORTED_ERROR_CODE,
204		};
205		const METHOD_NOT_FOUND: i32 = -32601;
206
207		let ClientError::RpcError(subxt::rpcs::Error::User(e)) = self else {
208			return None;
209		};
210		match e.code {
211			METHOD_NOT_FOUND => Some(RecordedUnavailable::MethodMissing),
212			CALL_RECORDED_DENIED_ERROR_CODE => Some(RecordedUnavailable::Denied),
213			CALL_RECORDED_UNSUPPORTED_ERROR_CODE => Some(RecordedUnavailable::NoRecorder),
214			_ => None,
215		}
216	}
217}
218
219/// Why a node cannot service `state_callRecorded`; the variants differ in fallback log detail.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub(crate) enum RecordedUnavailable {
222	/// The node's binary predates `state_callRecorded` (`-32601`).
223	MethodMissing,
224	/// `state_callRecorded` is unsafe and disabled on this node (`CALL_RECORDED_DENIED`).
225	Denied,
226	/// The node registers no proof-size recorder (e.g. a solochain).
227	NoRecorder,
228}
229
230impl RecordedUnavailable {
231	/// Whether a recorder-less replay under this reason may have dropped traces.
232	pub(crate) fn is_degraded(self) -> bool {
233		matches!(self, Self::MethodMissing | Self::Denied)
234	}
235}
236
237// Direct `From` impls so `?` can lift sub-error variants without an explicit `subxt::Error::from`.
238macro_rules! impl_from_subxt_subtype {
239	($($ty:ty),* $(,)?) => {
240		$(
241			impl From<$ty> for ClientError {
242				fn from(err: $ty) -> Self {
243					ClientError::SubxtError(err.into())
244				}
245			}
246		)*
247	};
248}
249
250impl_from_subxt_subtype!(
251	subxt::error::OnlineClientAtBlockError,
252	subxt::error::OnlineClientError,
253	subxt::error::BackendError,
254	subxt::error::BlockError,
255	subxt::error::BlocksError,
256	subxt::error::RuntimeApiError,
257	subxt::error::EventsError,
258	subxt::error::ExtrinsicError,
259	subxt::error::ConstantError,
260	subxt::error::StorageError,
261	subxt::error::StorageValueError,
262);
263
264const LOG_TARGET: &str = "eth-rpc::client";
265const LOG_TARGET_SUBSCRIPTION: &str = "eth-rpc::subscription";
266
267const REVERT_CODE: i32 = 3;
268
269const NOTIFIER_CAPACITY: usize = 16;
270
271impl From<ClientError> for ErrorObjectOwned {
272	fn from(err: ClientError) -> Self {
273		match err {
274			ClientError::SubxtError(subxt::Error::BackendError(
275				subxt::error::BackendError::Rpc(subxt::error::RpcError::ClientError(
276					subxt::rpcs::Error::User(err),
277				)),
278			)) |
279			ClientError::RpcError(subxt::rpcs::Error::User(err)) => {
280				ErrorObjectOwned::owned::<Vec<u8>>(err.code, err.message, None)
281			},
282			ClientError::TransactError(EthTransactError::Data(data)) => {
283				let msg = match decode_revert_reason(&data) {
284					Some(reason) => format!("execution reverted: {reason}"),
285					None => "execution reverted".to_string(),
286				};
287
288				let data = format!("0x{}", hex::encode(data));
289				ErrorObjectOwned::owned::<String>(REVERT_CODE, msg, Some(data))
290			},
291			ClientError::TransactError(EthTransactError::Message(msg)) => {
292				ErrorObjectOwned::owned::<String>(CALL_EXECUTION_FAILED_CODE, msg, None)
293			},
294			_ => {
295				ErrorObjectOwned::owned::<String>(CALL_EXECUTION_FAILED_CODE, err.to_string(), None)
296			},
297		}
298	}
299}
300
301/// A client that connects to a substrate node and provides Ethereum-compatible RPC functionality.
302#[derive(Clone)]
303pub struct Client {
304	api: OnlineClient<SrcChainConfig>,
305	rpc_client: RpcClient,
306	rpc: LegacyRpcMethods<RpcConfigFor<SrcChainConfig>>,
307	receipt_provider: ReceiptProvider,
308	block_provider: SubxtBlockInfoProvider,
309	fee_history_provider: FeeHistoryProvider,
310	chain_id: u64,
311	max_block_weight: Weight,
312	/// Whether the node has automine enabled.
313	automine: bool,
314	/// A notifier, that informs subscribers of new best blocks.
315	block_notifier: Option<tokio::sync::broadcast::Sender<H256>>,
316	/// A lock to ensure only one subscription can perform write operations at a time.
317	subscription_lock: Arc<Mutex<()>>,
318
319	/// Block subscription sender side.
320	block_subscription_tx: tokio::sync::broadcast::Sender<BlockV1>,
321	/// Log subscription sender side.
322	log_subscription_tx: tokio::sync::broadcast::Sender<Log>,
323	/// Whether archive mode is enabled
324	is_archive: bool,
325	/// Whether historic backfill has completed. `false` if not started or in progress.
326	backfill_complete: Arc<AtomicBool>,
327	/// Queue for backfilling blocks missed during subscription reconnects.
328	subscription_gap_queue: SubscriptionGapQueue,
329	/// Hands out the version-aware runtime API of specific blocks.
330	runtime_api_provider: VersionAwareRuntimeApiProvider,
331}
332
333/// A request to backfill a range of missed blocks (both bounds inclusive).
334pub(crate) struct GapFillRequest {
335	pub from_inclusive: SubstrateBlockNumber,
336	pub to_inclusive: SubstrateBlockNumber,
337}
338
339/// Queues gap-fill requests for blocks missed during subscription reconnects.
340#[derive(Clone)]
341pub(crate) struct SubscriptionGapQueue {
342	/// Sender half of the gap-fill queue.
343	tx: mpsc::Sender<GapFillRequest>,
344	/// Queued + in-flight gap fills. Channel length alone is insufficient
345	/// because it drops to zero as soon as the receiver dequeues the item.
346	pending: Arc<AtomicUsize>,
347}
348
349impl SubscriptionGapQueue {
350	pub(crate) fn new() -> (Self, mpsc::Receiver<GapFillRequest>) {
351		// Each reconnect produces one gap-fill request for the entire missed range,
352		// so 32 allows for 32 rapid disconnects before the consumer processes any.
353		let (tx, rx) = mpsc::channel(32);
354		(Self { tx, pending: Arc::new(AtomicUsize::new(0)) }, rx)
355	}
356
357	/// If `current` is not consecutive to `last`, queue a gap-fill for the missing range.
358	pub fn detect_and_queue(&self, current: SubstrateBlockNumber, last: SubstrateBlockNumber) {
359		if current.saturating_sub(last) <= 1 {
360			return;
361		}
362
363		let from_inclusive = current.saturating_sub(1);
364		let to_inclusive = last.saturating_add(1);
365		let gap_len = from_inclusive.saturating_sub(to_inclusive) + 1;
366		self.pending.fetch_add(1, Ordering::Release);
367		match self.tx.try_send(GapFillRequest { from_inclusive, to_inclusive }) {
368			Ok(_) => {
369				log::info!(target: LOG_TARGET,
370					"๐Ÿ”„ Subscription gap queue: queued #{from_inclusive} down to #{to_inclusive} ({gap_len} blocks)");
371			},
372			Err(err) => {
373				self.pending.fetch_sub(1, Ordering::Release);
374				log::warn!(target: LOG_TARGET,
375					"๐Ÿ”„ Subscription gap queue error, dropping #{from_inclusive}..#{to_inclusive} ({gap_len} blocks): {err}");
376			},
377		}
378	}
379
380	/// Mark one request as processed.
381	pub fn mark_done(&self) {
382		let res = self
383			.pending
384			.fetch_update(Ordering::AcqRel, Ordering::Acquire, |v| v.checked_sub(1));
385		if res.is_err() {
386			debug_assert!(false, "subscription gap queue pending counter underflowed");
387			log::error!(target: LOG_TARGET,
388				"๐Ÿ”„ Subscription gap queue pending counter underflow, delete the database and restart with --eth-pruning=archive to resync");
389		}
390	}
391
392	/// Returns `true` if there are pending gap-fill requests.
393	pub fn has_pending(&self) -> bool {
394		self.pending.load(Ordering::Acquire) > 0
395	}
396}
397
398/// Returns the first EVM block number for main and test nets, `None` otherwise.
399fn known_first_evm_block_for_chain(chain_id: u64) -> Option<u64> {
400	match chain_id {
401		420420417 => Some(4_367_914),  // Paseo Asset Hub
402		420420418 => Some(12_234_156), // Kusama Asset Hub
403		420420419 => Some(11_405_259), // Polkadot Asset Hub
404		420420421 => Some(13_169_391), // Westend Asset Hub
405		_ => None,
406	}
407}
408
409/// Fetch the max block weight from the substrate chain.
410async fn max_block_weight(api: &OnlineClient<SrcChainConfig>) -> Result<Weight, ClientError> {
411	let query = subxt_client::constants().system().block_weights().unvalidated();
412	let at_block = api.at_current_block().await?;
413	let weights = at_block.constants().entry(query)?;
414	let max_block = weights.per_class.normal.max_extrinsic.unwrap_or(weights.max_block);
415	Ok(max_block.0)
416}
417
418/// Get the automine status from the node.
419async fn get_automine(rpc_client: &RpcClient) -> bool {
420	match rpc_client.request::<bool>("getAutomine", rpc_params![]).await {
421		Ok(val) => val,
422		Err(err) => {
423			log::info!(target: LOG_TARGET, "Node does not have getAutomine RPC. Defaulting to automine=false. error: {err:?}");
424			false
425		},
426	}
427}
428
429/// Connect to a node at the given URL, and return the underlying API, RPC client, and legacy RPC
430/// clients.
431pub async fn connect(
432	node_rpc_url: &str,
433	max_request_size: u32,
434	max_response_size: u32,
435) -> Result<
436	(OnlineClient<SrcChainConfig>, RpcClient, LegacyRpcMethods<RpcConfigFor<SrcChainConfig>>),
437	ClientError,
438> {
439	log::info!(target: LOG_TARGET, "๐ŸŒ Connecting to node at: {node_rpc_url} ...");
440	let rpc_client = ReconnectingRpcClient::builder()
441		.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
442		.max_request_size(max_request_size)
443		.max_response_size(max_response_size)
444		.build(node_rpc_url.to_string())
445		.await?;
446	let rpc_client = RpcClient::new(rpc_client);
447	log::info!(target: LOG_TARGET, "๐ŸŒŸ Connected to node at: {node_rpc_url}");
448
449	// Pin the legacy backend explicitly. Since subxt 0.50, from_rpc_client defaults
450	// to the CombinedBackend, which routes block streams and header fetches through
451	// the chainHead protocol; its follow restarts and pinning limits stall receipt
452	// indexing under load (blocks become unresolvable once unpinned).
453	let backend = Arc::new(LegacyBackend::builder().build(rpc_client.clone()));
454	let api = OnlineClient::<SrcChainConfig>::from_backend(backend).await?;
455	let rpc = LegacyRpcMethods::<RpcConfigFor<SrcChainConfig>>::new(rpc_client.clone());
456	Ok((api, rpc_client, rpc))
457}
458
459impl Client {
460	/// Create a new client instance.
461	pub(crate) async fn new(
462		api: OnlineClient<SrcChainConfig>,
463		rpc_client: RpcClient,
464		rpc: LegacyRpcMethods<RpcConfigFor<SrcChainConfig>>,
465		block_provider: SubxtBlockInfoProvider,
466		receipt_provider: ReceiptProvider,
467		is_archive: bool,
468		subscription_gap_queue: SubscriptionGapQueue,
469		runtime_api_provider: VersionAwareRuntimeApiProvider,
470		chain_id: u64,
471	) -> Result<Self, ClientError> {
472		let (max_block_weight, automine) = tokio::try_join!(max_block_weight(&api), async {
473			Ok(get_automine(&rpc_client).await)
474		},)?;
475
476		// Compute the very first capabilities so that the provider's cache has an anchor which
477		// the subscriptions grow forward and the backfill grows backward.
478		let latest_finalized_block = block_provider.latest_finalized_block().await;
479		runtime_api_provider.at(latest_finalized_block.block_hash()).await?;
480
481		// Fall back to 0 when the hardcoded value exceeds the current best block (e.g. zombienet
482		// reusing a known chain ID) and backward sync is disabled.
483		if !is_archive {
484			if let Some(known) = known_first_evm_block_for_chain(chain_id) {
485				let best = block_provider.latest_block_number().await;
486				if known > best {
487					log::debug!(
488						target: LOG_TARGET,
489						"Hardcoded first EVM block {known} exceeds best block {best} \
490						 for chain {chain_id}, defaulting to 0"
491					);
492					receipt_provider.set_first_evm_block(0).await?;
493				}
494			}
495		}
496
497		let client = Self {
498			api,
499			rpc_client,
500			rpc,
501			receipt_provider,
502			block_provider,
503			fee_history_provider: FeeHistoryProvider::default(),
504			chain_id,
505			max_block_weight,
506			automine,
507			block_notifier: automine
508				.then(|| tokio::sync::broadcast::channel::<H256>(NOTIFIER_CAPACITY).0),
509			subscription_lock: Arc::new(Mutex::new(())),
510			block_subscription_tx: tokio::sync::broadcast::channel(256).0,
511			log_subscription_tx: tokio::sync::broadcast::channel(1000).0,
512			is_archive,
513			backfill_complete: Arc::new(AtomicBool::new(false)),
514			subscription_gap_queue,
515			runtime_api_provider,
516		};
517
518		Ok(client)
519	}
520
521	/// Mark historic backfill as complete.
522	pub(crate) fn mark_backfill_complete(&self) {
523		self.backfill_complete.store(true, Ordering::Release);
524	}
525
526	/// Advance the sync_state head label if safe to do so.
527	/// Requires: archive mode, historic backfill complete, and no pending gap fills.
528	async fn advance_sync_head(&self, block_number: SubstrateBlockNumber, hash: H256) {
529		if !self.is_archive ||
530			!self.backfill_complete.load(Ordering::Acquire) ||
531			self.subscription_gap_queue.has_pending()
532		{
533			return;
534		}
535
536		if let Err(err) = self
537			.receipt_provider
538			.advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(block_number, hash))
539			.await
540		{
541			log::warn!(target: LOG_TARGET, "Failed to advance sync head: {err:?}");
542		}
543	}
544
545	/// Creates a block notifier instance.
546	pub fn create_block_notifier(&mut self) {
547		self.block_notifier = Some(tokio::sync::broadcast::channel::<H256>(NOTIFIER_CAPACITY).0);
548	}
549
550	/// Sets a block notifier
551	pub fn set_block_notifier(&mut self, notifier: Option<tokio::sync::broadcast::Sender<H256>>) {
552		self.block_notifier = notifier;
553	}
554
555	pub(crate) fn api(&self) -> &OnlineClient<SrcChainConfig> {
556		&self.api
557	}
558
559	pub(crate) fn receipt_provider(&self) -> &ReceiptProvider {
560		&self.receipt_provider
561	}
562
563	pub(crate) fn block_provider(&self) -> &SubxtBlockInfoProvider {
564		&self.block_provider
565	}
566
567	pub(crate) fn subscription_gap_queue(&self) -> &SubscriptionGapQueue {
568		&self.subscription_gap_queue
569	}
570
571	/// The earliest block number where the ReviveApi is available.
572	/// Resolution order: in-memory value > known-networks table > 0.
573	fn earliest_block_number(&self) -> SubstrateBlockNumber {
574		self.receipt_provider
575			.first_evm_block()
576			.or_else(|| known_first_evm_block_for_chain(self.chain_id))
577			.unwrap_or(0)
578	}
579
580	/// Subscribe to new blocks, and execute the async closure for each block.
581	async fn subscribe_new_blocks<F, Fut>(
582		&self,
583		subscription_type: SubscriptionType,
584		callback: F,
585	) -> Result<(), ClientError>
586	where
587		F: Fn(SubstrateBlock) -> Fut + Send + Sync,
588		Fut: std::future::Future<Output = Result<(), ClientError>> + Send,
589	{
590		let mut block_stream = match subscription_type {
591			SubscriptionType::BestBlocks => self.api.stream_best_blocks().await,
592			SubscriptionType::FinalizedBlocks => self.api.stream_blocks().await,
593		}
594		.inspect_err(|err| {
595			log::error!(target: LOG_TARGET, "Failed to subscribe to blocks: {err:?}");
596		})?;
597
598		let mut last_finalized_seen: Option<SubstrateBlockNumber> = None;
599
600		while let Some(block) = block_stream.next().await {
601			let block = match block {
602				Ok(block) => block,
603				Err(err) => {
604					let err: subxt::Error = err.into();
605					if err.is_disconnected_will_reconnect() {
606						log::warn!(
607							target: LOG_TARGET,
608							"The RPC connection was lost and we may have missed a few blocks \
609							({subscription_type:?}, last finalized: {last_finalized_seen:?}): {err:?}"
610						);
611						continue;
612					}
613
614					log::error!(target: LOG_TARGET, "Failed to fetch block ({subscription_type:?}): {err:?}");
615					return Err(err.into());
616				},
617			};
618
619			// Resolution fails for pruned/retracted blocks and on transient RPC errors;
620			// erroring out here would kill the essential subscription task and with it the
621			// whole server. Skip instead: a skipped finalized block doesn't advance
622			// `last_finalized_seen`, so the gap filler backfills it.
623			let block = match block.at().await {
624				Ok(block) => block,
625				Err(err) => {
626					log::warn!(
627						target: LOG_TARGET,
628						"Failed to resolve streamed {subscription_type:?} block #{} ({:?}), skipping: {err:?}",
629						block.number(),
630						block.hash(),
631					);
632					continue;
633				},
634			};
635
636			// Acquire lock to ensure only one subscription can perform write operations at a time
637			let _guard = self.subscription_lock.lock().await;
638
639			let block_number = block.block_number();
640
641			// Only check finalized blocks for gaps.
642			if subscription_type == SubscriptionType::FinalizedBlocks {
643				if let Some(last) = last_finalized_seen {
644					self.subscription_gap_queue.detect_and_queue(block_number, last);
645				}
646				// Update unconditionally โ€” a callback failure doesn't mean the block was missed.
647				last_finalized_seen = Some(block_number);
648			}
649
650			log::trace!(target: LOG_TARGET_SUBSCRIPTION, "โณ Processing {subscription_type:?} block: {block_number}");
651			if let Err(err) = callback(block).await {
652				log::error!(target: LOG_TARGET, "Failed to process block {block_number}: {err:?}");
653			} else {
654				log::trace!(target: LOG_TARGET_SUBSCRIPTION, "โœ… Processed {subscription_type:?} block: {block_number}");
655			}
656		}
657
658		log::info!(target: LOG_TARGET, "Block subscription ended");
659		Ok(())
660	}
661
662	/// Extract receipts from a block, persist them and update fee history.
663	async fn process_block(
664		&self,
665		block: &SubstrateBlock,
666	) -> Result<(BlockV1, Vec<ReceiptInfo>), ClientError> {
667		let block_number = block.block_number();
668		let hash = block.block_hash();
669
670		macro_rules! time {
671			($label:expr, $expr:expr) => {{
672				let t = std::time::Instant::now();
673				let r = $expr;
674				log::trace!(
675					target: LOG_TARGET,
676					"โฑ๏ธ #{block_number} {}: {:?}",
677					$label, t.elapsed(),
678				);
679				r
680			}};
681		}
682
683		let eth_block = time!(
684			"eth_block",
685			self.runtime_api(hash)
686				.await?
687				.eth_block()
688				.ok_or(ClientError::UnsupportedRuntimeApiMethod("eth_block"))?
689				.await?
690		);
691		let receipts = time!(
692			"receipts_from_block",
693			self.receipt_provider.receipts_from_block(block, eth_block.hash).await?
694		);
695		time!(
696			"insert_block_receipts",
697			self.receipt_provider
698				.insert_block_receipts(block, &receipts, &eth_block.hash)
699				.await?
700		);
701
702		let (_, receipt_infos): (Vec<_>, Vec<_>) = receipts.into_iter().unzip();
703		self.fee_history_provider.update_fee_history(&eth_block, &receipt_infos).await;
704
705		Ok((eth_block, receipt_infos))
706	}
707
708	/// Start the block subscription, and populate the block cache.
709	pub async fn subscribe_and_cache_new_blocks(
710		&self,
711		subscription_type: SubscriptionType,
712	) -> Result<(), ClientError> {
713		log::info!(target: LOG_TARGET, "๐Ÿ”Œ Subscribing to new blocks ({subscription_type:?})");
714		self.subscribe_new_blocks(subscription_type, |block| async {
715			let hash = block.block_hash();
716
717			match subscription_type {
718				SubscriptionType::BestBlocks => {
719					let (eth_block, _) = self.process_block(&block).await?;
720					self.block_provider.update_latest(Arc::new(block), subscription_type).await;
721
722					if let Some(sender) = &self.block_notifier {
723						if sender.receiver_count() > 0 {
724							let _ = sender.send(hash);
725						}
726					}
727					if self.block_subscription_tx.receiver_count() > 0 {
728						let _ = self.block_subscription_tx.send(eth_block);
729					}
730				},
731				SubscriptionType::FinalizedBlocks => {
732					let block_number = block.block_number();
733					let (receipt_infos, eth_hash) = match self
734						.receipt_provider
735						.get_processed_eth_block_hash(block_number, hash)
736						.await
737					{
738						Some(eth_hash) => {
739							log::trace!(target: LOG_TARGET_SUBSCRIPTION,
740									"โฉ Finalized block #{block_number} already processed, \
741									 skipping extraction");
742							(None, eth_hash)
743						},
744						None => {
745							let (eth_block, infos) = self.process_block(&block).await?;
746							(Some(infos), eth_block.hash)
747						},
748					};
749
750					self.block_provider.update_latest(Arc::new(block), subscription_type).await;
751					self.advance_sync_head(block_number, hash).await;
752
753					if self.log_subscription_tx.receiver_count() > 0 {
754						let logs = match receipt_infos {
755							Some(infos) => infos.into_iter().flat_map(|r| r.logs).collect(),
756							None => {
757								self.receipt_provider
758									.logs_by_block_number(block_number, eth_hash)
759									.await?
760							},
761						};
762						for log in logs {
763							let _ = self.log_subscription_tx.send(log);
764						}
765					}
766				},
767			}
768
769			Ok(())
770		})
771		.await
772	}
773
774	/// Get the block hash for the given block number or tag.
775	pub async fn block_hash_for_tag(&self, at: BlockId) -> Result<SubstrateBlockHash, ClientError> {
776		match at {
777			BlockId::Hash(hash) => self
778				.resolve_substrate_hash(&H256::from(hash.block_hash.0))
779				.await
780				.ok_or(ClientError::EthereumBlockNotFound),
781			BlockId::Number(tag) => self
782				.block_by_number_or_tag(&tag)
783				.await?
784				.map(|block| block.block_hash())
785				.ok_or(ClientError::BlockNotFound),
786		}
787	}
788
789	/// Get a block for the specified hash or number.
790	pub async fn block_by_number_or_tag(
791		&self,
792		block: &BlockNumberOrTag,
793	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
794		match block {
795			BlockNumberOrTag::Number(n) => {
796				let n = (*n).try_into().map_err(|_| ClientError::ConversionFailed)?;
797				self.block_by_number(n).await
798			},
799			BlockNumberOrTag::Finalized | BlockNumberOrTag::Safe => {
800				let block = self.block_provider.latest_finalized_block().await;
801				Ok(Some(block))
802			},
803			BlockNumberOrTag::Earliest => self.block_by_number(self.earliest_block_number()).await,
804			BlockNumberOrTag::Latest | BlockNumberOrTag::Pending => {
805				let block = self.block_provider.latest_block().await;
806				Ok(Some(block))
807			},
808		}
809	}
810
811	/// Get the block for the given block number or tag, or `None` if it is not known.
812	///
813	/// Prefer this over resolving a hash with [`Self::block_hash_for_tag`] and then fetching the
814	/// block by that hash, which fetches the same block twice.
815	pub async fn block_for_tag(
816		&self,
817		at: BlockId,
818	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
819		match at {
820			BlockId::Hash(hash) => {
821				let Some(hash) = self.resolve_substrate_hash(&H256::from(hash.block_hash.0)).await
822				else {
823					return Ok(None);
824				};
825				self.block_by_hash(&hash).await
826			},
827			BlockId::Number(tag) => self.block_by_number_or_tag(&tag).await,
828		}
829	}
830
831	/// Resolve a [`BlockNumberOrTag`] to a concrete block number.
832	async fn resolve_tag_to_number(&self, tag: BlockNumberOrTag) -> Result<U256, ClientError> {
833		match tag {
834			BlockNumberOrTag::Number(n) => Ok(U256::from(n)),
835			BlockNumberOrTag::Earliest => Ok(U256::from(self.earliest_block_number())),
836			_ => Ok(self
837				.block_by_number_or_tag(&tag)
838				.await?
839				.ok_or(ClientError::BlockNotFound)?
840				.block_number()
841				.into()),
842		}
843	}
844
845	/// Get the storage API for the given block.
846	pub async fn storage_api(&self, block_hash: H256) -> Result<StorageApi, ClientError> {
847		Ok(StorageApi::new(self.api.at_block(block_hash).await?))
848	}
849
850	/// Get the version-aware runtime API for the given block.
851	pub async fn runtime_api(
852		&self,
853		block_hash: H256,
854	) -> Result<VersionAwareRuntimeApi, ClientError> {
855		self.runtime_api_provider.at(block_hash).await
856	}
857
858	/// Get the latest finalized block.
859	pub async fn latest_finalized_block(&self) -> Arc<SubstrateBlock> {
860		self.block_provider.latest_finalized_block().await
861	}
862
863	/// Get the latest best block.
864	pub async fn latest_block(&self) -> Arc<SubstrateBlock> {
865		self.block_provider.latest_block().await
866	}
867
868	/// Submit an ethereum transaction and return a stream of transaction status updates.
869	async fn submit_transaction(
870		&self,
871		call: subxt::tx::StaticPayload<EthTransact>,
872	) -> Result<StreamOfResults<TransactionStatus<SubstrateBlockHash>>, ClientError> {
873		let at_block = self.api.at_current_block().await?;
874		let ext = at_block.tx().create_unsigned(&call.unvalidated()).map_err(ClientError::from)?;
875
876		let sub = self
877			.rpc_client
878			.subscribe(
879				"author_submitAndWatchExtrinsic",
880				rpc_params![to_hex(ext.encoded())],
881				"author_unwatchExtrinsic",
882			)
883			.await?;
884
885		let sub = sub.map_err(|e| e.into());
886		Ok(StreamOf::new(Box::pin(sub)))
887	}
888
889	/// Expose the transaction API.
890	pub async fn submit(
891		&self,
892		call: subxt::tx::StaticPayload<EthTransact>,
893	) -> Result<TransactionStatus<SubstrateBlockHash>, ClientError> {
894		let mut progress = self.submit_transaction(call).await.inspect_err(|err| {
895			log::debug!(target: LOG_TARGET, "Failed to submit transaction: {err:?}");
896		})?;
897
898		tokio::time::timeout(Duration::from_secs(5), async {
899			if let Some(status) = progress.next().await {
900				match status {
901					Ok(
902						tx @ (TransactionStatus::Future |
903						TransactionStatus::Ready |
904						// Add other events that follow Ready here for completeness,
905						// but they can be ignored.
906						TransactionStatus::Broadcast(_) |
907						TransactionStatus::InBlock(_) |
908						TransactionStatus::FinalityTimeout(_) |
909						TransactionStatus::Retracted(_) |
910						TransactionStatus::Finalized(_)),
911					) => {
912						return Ok(tx);
913					},
914					Ok(
915						tx @ (TransactionStatus::Usurped(_) |
916						TransactionStatus::Dropped |
917						TransactionStatus::Invalid),
918					) => {
919						return Err(ClientError::SubmitError(tx.into()));
920					},
921					Err(err) => {
922						log::debug!(target: LOG_TARGET, "Transaction submission failed: {err:?}");
923						return Err(ClientError::from(err));
924					},
925				}
926			}
927			return Err(ClientError::SubmitError(SubmitError::StreamEnded));
928		})
929		.await
930		.map_err(|_| ClientError::Timeout)?
931	}
932
933	/// Get an EVM transaction receipt by hash.
934	pub async fn receipt(&self, tx_hash: &H256) -> Option<ReceiptInfo> {
935		self.receipt_provider.receipt_by_hash(tx_hash).await
936	}
937
938	/// Get all transaction receipts for the given block.
939	///
940	/// Returns `None` if the block does not exist.
941	pub async fn block_receipts(
942		&self,
943		at: BlockId,
944	) -> Result<Option<Vec<ReceiptInfo>>, ClientError> {
945		let Some(block) = self.block_for_tag(at).await? else {
946			return Ok(None);
947		};
948		let receipts = self.receipt_provider.block_receipts(&block).await?;
949		Ok(Some(receipts.into_iter().map(|(_, receipt)| receipt).collect()))
950	}
951
952	/// Get The post dispatch weight associated with this Ethereum transaction hash.
953	pub async fn post_dispatch_weight(&self, tx_hash: &H256) -> Option<Weight> {
954		use crate::subxt_client::system::events::ExtrinsicSuccess;
955		let ReceiptInfo { block_hash, transaction_index, .. } = self.receipt(tx_hash).await?;
956		let block_hash = self.resolve_substrate_hash(&block_hash).await?;
957		let block = self.block_provider.block_by_hash(&block_hash).await.ok()??;
958		let extrinsics = block.extrinsics().fetch().await.ok()?;
959		let ext = extrinsics.iter().nth(transaction_index.as_u32() as _)?.ok()?;
960		let event = ext.events().await.ok()?.find_first::<ExtrinsicSuccess>()?.ok()?;
961		Some(event.dispatch_info.weight.0)
962	}
963
964	pub async fn sync_state(
965		&self,
966	) -> Result<sc_rpc::system::SyncState<SubstrateBlockNumber>, ClientError> {
967		let client = self.rpc_client.clone();
968		let sync_state: sc_rpc::system::SyncState<SubstrateBlockNumber> =
969			client.request("system_syncState", Default::default()).await?;
970		Ok(sync_state)
971	}
972
973	/// Get the syncing status of the chain.
974	pub async fn syncing(&self) -> Result<SyncingStatus, ClientError> {
975		let health = self.rpc.system_health().await?;
976
977		let status = if health.is_syncing {
978			let sync_state = self.sync_state().await?;
979			SyncingStatus::SyncingProgress(SyncingProgress {
980				current_block: Some(sync_state.current_block.into()),
981				highest_block: Some(sync_state.highest_block.into()),
982				starting_block: Some(sync_state.starting_block.into()),
983			})
984		} else {
985			SyncingStatus::Bool(false)
986		};
987
988		Ok(status)
989	}
990
991	/// Get an EVM transaction receipt by hash.
992	pub async fn receipt_by_hash_and_index(
993		&self,
994		block_hash: &H256,
995		transaction_index: usize,
996	) -> Option<ReceiptInfo> {
997		self.receipt_provider
998			.receipt_by_block_hash_and_index(block_hash, transaction_index)
999			.await
1000	}
1001
1002	pub async fn signed_tx_by_hash(&self, tx_hash: &H256) -> Option<TransactionSigned> {
1003		self.receipt_provider.signed_tx_by_hash(tx_hash).await
1004	}
1005
1006	/// Get receipts count per block.
1007	pub async fn receipts_count_per_block(&self, block_hash: &SubstrateBlockHash) -> Option<usize> {
1008		self.receipt_provider.receipts_count_per_block(block_hash).await
1009	}
1010
1011	/// Get an EVM transaction receipt by specified Ethereum block hash.
1012	pub async fn receipt_by_ethereum_hash_and_index(
1013		&self,
1014		ethereum_hash: &H256,
1015		transaction_index: usize,
1016	) -> Option<ReceiptInfo> {
1017		// Fallback: use hash as Substrate hash if Ethereum hash cannot be resolved
1018		let substrate_hash =
1019			self.resolve_substrate_hash(ethereum_hash).await.unwrap_or_else(|| {
1020				log::trace!(target: LOG_TARGET,
1021					"receipt_by_ethereum_hash_and_index: no ETH-to-substrate mapping for \
1022					 {ethereum_hash:?}, falling back to substrate hash lookup");
1023				*ethereum_hash
1024			});
1025		self.receipt_by_hash_and_index(&substrate_hash, transaction_index).await
1026	}
1027
1028	/// Get the system health.
1029	pub async fn system_health(&self) -> Result<SystemHealth, ClientError> {
1030		let health = self.rpc.system_health().await?;
1031		Ok(health)
1032	}
1033
1034	/// Get the block number of the latest block.
1035	pub async fn block_number(&self) -> Result<SubstrateBlockNumber, ClientError> {
1036		let latest_block = self.block_provider.latest_block().await;
1037		Ok(latest_block.block_number())
1038	}
1039
1040	/// Get a block by hash
1041	pub async fn block_by_hash(
1042		&self,
1043		hash: &SubstrateBlockHash,
1044	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
1045		self.block_provider.block_by_hash(hash).await
1046	}
1047
1048	/// Resolve Ethereum block hash to Substrate block hash, then get the block.
1049	/// This method provides the abstraction layer needed by the RPC APIs.
1050	pub async fn resolve_substrate_hash(&self, ethereum_hash: &H256) -> Option<H256> {
1051		self.receipt_provider.get_substrate_hash(ethereum_hash).await
1052	}
1053
1054	/// Resolve Substrate block hash to Ethereum block hash, then get the block.
1055	/// This method provides the abstraction layer needed by the RPC APIs.
1056	pub async fn resolve_ethereum_hash(&self, substrate_hash: &H256) -> Option<H256> {
1057		self.receipt_provider.get_ethereum_hash(substrate_hash).await
1058	}
1059
1060	/// Get a block by Ethereum hash with automatic resolution to Substrate hash.
1061	/// Falls back to treating the hash as a Substrate hash if no mapping exists.
1062	pub async fn block_by_ethereum_hash(
1063		&self,
1064		ethereum_hash: &H256,
1065	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
1066		// First try to resolve the Ethereum hash to a Substrate hash
1067		if let Some(substrate_hash) = self.resolve_substrate_hash(ethereum_hash).await {
1068			return self.block_by_hash(&substrate_hash).await;
1069		}
1070
1071		// Fallback: treat the provided hash as a Substrate hash (backward compatibility)
1072		log::trace!(target: LOG_TARGET,
1073			"block_by_ethereum_hash: no ETH-to-substrate mapping for {ethereum_hash:?}, \
1074			 falling back to substrate hash lookup");
1075		self.block_by_hash(ethereum_hash).await
1076	}
1077
1078	/// Get a block by number
1079	pub async fn block_by_number(
1080		&self,
1081		block_number: SubstrateBlockNumber,
1082	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
1083		self.block_provider.block_by_number(block_number).await
1084	}
1085
1086	/// Get a block hash for the given block number.
1087	pub async fn get_block_hash(
1088		&self,
1089		block_number: SubstrateBlockNumber,
1090	) -> Result<Option<SubstrateBlockHash>, ClientError> {
1091		let maybe_block = self.block_provider.block_by_number(block_number).await?;
1092		Ok(maybe_block.map(|block| block.block_hash()))
1093	}
1094
1095	async fn tracing_block(
1096		&self,
1097		block_hash: H256,
1098	) -> Result<
1099		sp_runtime::generic::Block<
1100			sp_runtime::generic::Header<u32, sp_runtime::traits::BlakeTwo256>,
1101			sp_runtime::OpaqueExtrinsic,
1102		>,
1103		ClientError,
1104	> {
1105		let signed_block: Option<
1106			sp_runtime::generic::SignedBlock<
1107				sp_runtime::generic::Block<
1108					sp_runtime::generic::Header<u32, sp_runtime::traits::BlakeTwo256>,
1109					sp_runtime::OpaqueExtrinsic,
1110				>,
1111			>,
1112		> = self.rpc_client.request("chain_getBlock", rpc_params![block_hash]).await?;
1113
1114		Ok(signed_block.ok_or(ClientError::BlockNotFound)?.block)
1115	}
1116
1117	/// Get the transaction traces for the given block.
1118	pub async fn trace_block_by_number(
1119		&self,
1120		at: BlockNumberOrTag,
1121		config: TracerTypeV1,
1122	) -> Result<Vec<TransactionTrace>, ClientError> {
1123		if self.receipt_provider.is_before_earliest_block(&at) {
1124			return Ok(vec![]);
1125		}
1126
1127		let block_hash = self.block_hash_for_tag(at.into()).await?;
1128		let block = self.tracing_block(block_hash).await?;
1129		let parent_hash = block.header().parent_hash;
1130		// Block 0 has no parent โ€” there is nothing to trace.
1131		if parent_hash == Default::default() {
1132			return Ok(vec![]);
1133		}
1134
1135		let CallRecordedOutput { value: trace_entries, degraded } = self
1136			.runtime_api(parent_hash)
1137			.await?
1138			.trace_block(block, config, block_hash)
1139			.ok_or(ClientError::UnsupportedRuntimeApiMethod("trace_block"))?
1140			.await?;
1141
1142		let mut hashes = self
1143			.receipt_provider
1144			.block_transaction_hashes(&block_hash)
1145			.await
1146			.ok_or(ClientError::EthExtrinsicNotFound)?;
1147
1148		let mut entries = trace_entries
1149			.into_iter()
1150			.filter_map(|(index, entry)| {
1151				let index = index as usize;
1152				let tx_hash = hashes.remove(&index)?;
1153				let outcome = match entry {
1154					TraceEntry::Traced(trace) => TraceOutcome::Trace(trace),
1155					TraceEntry::NotTraced => {
1156						TraceOutcome::Error(ClientError::TraceUnavailable.to_string())
1157					},
1158				};
1159				Some((index, TransactionTrace { tx_hash, outcome }))
1160			})
1161			.collect::<Vec<_>>();
1162
1163		if degraded {
1164			entries.extend(hashes.into_iter().map(|(index, tx_hash)| {
1165				let outcome = TraceOutcome::Error(ClientError::TraceUnavailable.to_string());
1166				(index, TransactionTrace { tx_hash, outcome })
1167			}));
1168			entries.sort_unstable_by_key(|(index, _)| *index);
1169		}
1170
1171		Ok(entries.into_iter().map(|(_, entry)| entry).collect())
1172	}
1173
1174	/// Get the transaction traces for the given transaction.
1175	pub async fn trace_transaction(
1176		&self,
1177		transaction_hash: H256,
1178		config: TracerTypeV1,
1179	) -> Result<TraceV1, ClientError> {
1180		let (block_hash, transaction_index) = self
1181			.receipt_provider
1182			.find_transaction(&transaction_hash)
1183			.await
1184			.ok_or(ClientError::EthExtrinsicNotFound)?;
1185		let transaction_index = transaction_index as u32;
1186
1187		let block = self.tracing_block(block_hash).await?;
1188		let parent_hash = block.header.parent_hash;
1189		let CallRecordedOutput { value: entry, degraded } = self
1190			.runtime_api(parent_hash)
1191			.await?
1192			.trace_tx(block, transaction_index, config, block_hash)
1193			.ok_or(ClientError::UnsupportedRuntimeApiMethod("trace_tx"))?
1194			.await?;
1195
1196		match entry {
1197			Some(TraceEntry::Traced(trace)) => Ok(trace),
1198			Some(TraceEntry::NotTraced) => Err(ClientError::TraceUnavailable),
1199			None if degraded => Err(ClientError::TraceUnavailable),
1200			None => Err(ClientError::EthExtrinsicNotFound),
1201		}
1202	}
1203
1204	/// Get the transaction traces for the given block.
1205	pub async fn trace_call(
1206		&self,
1207		transaction: GenericTransactionV1,
1208		block: BlockId,
1209		config: TracerTypeV1,
1210		state_overrides: Option<StateOverrideSetV1>,
1211	) -> Result<TraceV1, ClientError> {
1212		let block_hash = self.block_hash_for_tag(block).await?;
1213		let runtime_api = self.runtime_api(block_hash).await?;
1214		runtime_api
1215			.trace_call(transaction, config, state_overrides)
1216			.ok_or(ClientError::UnsupportedRuntimeApiMethod("trace_call"))?
1217			.await
1218	}
1219
1220	/// Get the EVM block for the given Substrate block.
1221	pub async fn evm_block(
1222		&self,
1223		block: Arc<SubstrateBlock>,
1224		hydrated_transactions: bool,
1225	) -> Option<BlockV1> {
1226		log::trace!(target: LOG_TARGET, "Get Ethereum block for hash {:?}", block.block_hash());
1227
1228		if self
1229			.receipt_provider
1230			.is_before_earliest_block(&BlockNumberOrTag::Number(block.block_number().into()))
1231		{
1232			log::trace!(target: LOG_TARGET,
1233				"Block #{} is before receipt floor, skipping", block.block_number());
1234			return None;
1235		}
1236
1237		// This could potentially fail under below circumstances:
1238		//  - state has been pruned
1239		//  - the block author cannot be obtained from the digest logs (highly unlikely)
1240		//  - the node we are targeting has an outdated revive pallet (or ETH block functionality is
1241		//    disabled)
1242		let eth_block_result = async {
1243			self.runtime_api(block.block_hash())
1244				.await?
1245				.eth_block()
1246				.ok_or(ClientError::UnsupportedRuntimeApiMethod("eth_block"))?
1247				.await
1248		}
1249		.await;
1250		match eth_block_result {
1251			Ok(mut eth_block) => {
1252				log::trace!(target: LOG_TARGET, "Ethereum block from runtime API hash {:?}", eth_block.hash);
1253
1254				if hydrated_transactions {
1255					// Hydrate the block.
1256					let tx_infos = self
1257						.receipt_provider
1258						.receipts_from_block(&block, eth_block.hash)
1259						.await
1260						.inspect_err(|err| {
1261							log::trace!(target: LOG_TARGET,
1262								"Failed to extract receipts for block #{}: {err:?}",
1263								block.block_number());
1264						})
1265						.unwrap_or_default()
1266						.into_iter()
1267						.map(|(signed_tx, receipt)| receipt.transaction_info(signed_tx))
1268						.collect::<Vec<_>>();
1269
1270					eth_block.transactions = HashesOrTransactionInfosV1::TransactionInfos(tx_infos);
1271				}
1272
1273				Some(eth_block)
1274			},
1275			Err(err) => {
1276				log::error!(target: LOG_TARGET, "Failed to get Ethereum block for hash {:?}: {err:?}", block.block_hash());
1277				None
1278			},
1279		}
1280	}
1281
1282	/// Get the chain ID.
1283	pub fn chain_id(&self) -> u64 {
1284		self.chain_id
1285	}
1286
1287	/// Get the Max Block Weight.
1288	pub fn max_block_weight(&self) -> Weight {
1289		self.max_block_weight
1290	}
1291
1292	/// Get the block notifier, if automine is enabled or Self::create_block_notifier was called.
1293	pub fn block_notifier(&self) -> Option<tokio::sync::broadcast::Sender<H256>> {
1294		self.block_notifier.clone()
1295	}
1296
1297	/// Get the logs matching the given filter.
1298	pub async fn logs(&self, filter: Option<Filter>) -> Result<Vec<Log>, ClientError> {
1299		let logs =
1300			self.receipt_provider
1301				.logs(filter, |tag| async move {
1302					self.resolve_tag_to_number(tag).await.map_err(Into::into)
1303				})
1304				.await
1305				.map_err(ClientError::LogFilterFailed)?;
1306
1307		Ok(logs)
1308	}
1309
1310	pub async fn fee_history(
1311		&self,
1312		block_count: u32,
1313		latest_block: BlockNumberOrTag,
1314		reward_percentiles: Option<Vec<f64>>,
1315	) -> Result<FeeHistoryResult, ClientError> {
1316		let Some(latest_block) = self.block_by_number_or_tag(&latest_block).await? else {
1317			return Err(ClientError::BlockNotFound);
1318		};
1319
1320		self.fee_history_provider
1321			.fee_history(block_count, latest_block.block_number(), reward_percentiles)
1322			.await
1323	}
1324
1325	/// Check if automine is enabled.
1326	pub fn is_automine(&self) -> bool {
1327		self.automine
1328	}
1329
1330	/// Get the automine status from the node.
1331	pub async fn get_automine(&self) -> bool {
1332		get_automine(&self.rpc_client).await
1333	}
1334
1335	/// Gets the block subscription rx side of the channel.
1336	pub fn get_block_subscription_rx(&self) -> tokio::sync::broadcast::Receiver<BlockV1> {
1337		self.block_subscription_tx.subscribe()
1338	}
1339
1340	/// Gets the log subscription rx side of the channel.
1341	pub fn get_log_subscription_rx(&self) -> tokio::sync::broadcast::Receiver<Log> {
1342		self.log_subscription_tx.subscribe()
1343	}
1344}
1345
1346fn to_hex(bytes: impl AsRef<[u8]>) -> String {
1347	format!("0x{}", hex::encode(bytes.as_ref()))
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352	use super::*;
1353	use sc_rpc_api::state::error::{
1354		CALL_RECORDED_DENIED_ERROR_CODE, CALL_RECORDED_UNSUPPORTED_ERROR_CODE,
1355	};
1356	use subxt::rpcs::UserError;
1357
1358	fn rpc_user_error(code: i32) -> ClientError {
1359		ClientError::RpcError(subxt::rpcs::Error::User(UserError {
1360			code,
1361			message: "..".to_string(),
1362			data: None,
1363		}))
1364	}
1365
1366	#[test]
1367	fn missing_method_is_version_skew() {
1368		assert_eq!(
1369			rpc_user_error(-32601).recorded_unavailable_reason(),
1370			Some(RecordedUnavailable::MethodMissing),
1371		);
1372	}
1373
1374	#[test]
1375	fn unsafe_denied_is_a_fallback_reason() {
1376		assert_eq!(
1377			rpc_user_error(CALL_RECORDED_DENIED_ERROR_CODE).recorded_unavailable_reason(),
1378			Some(RecordedUnavailable::Denied),
1379		);
1380	}
1381
1382	#[test]
1383	fn no_recorder_code_is_expected() {
1384		assert_eq!(
1385			rpc_user_error(CALL_RECORDED_UNSUPPORTED_ERROR_CODE).recorded_unavailable_reason(),
1386			Some(RecordedUnavailable::NoRecorder),
1387		);
1388	}
1389
1390	#[test]
1391	fn genuine_errors_do_not_trigger_fallback() {
1392		assert!(rpc_user_error(-32000).recorded_unavailable_reason().is_none());
1393		assert!(rpc_user_error(-32602).recorded_unavailable_reason().is_none());
1394		assert!(ClientError::EthExtrinsicNotFound.recorded_unavailable_reason().is_none());
1395		assert!(ClientError::BlockNotFound.recorded_unavailable_reason().is_none());
1396	}
1397}