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 chain ID from the substrate chain.
410async fn chain_id(api: &OnlineClient<SrcChainConfig>) -> Result<u64, ClientError> {
411	let query = subxt_client::constants().revive().chain_id().unvalidated();
412	let at_block = api.at_current_block().await?;
413	at_block.constants().entry(query).map_err(|err| err.into())
414}
415
416/// Fetch the max block weight from the substrate chain.
417async fn max_block_weight(api: &OnlineClient<SrcChainConfig>) -> Result<Weight, ClientError> {
418	let query = subxt_client::constants().system().block_weights().unvalidated();
419	let at_block = api.at_current_block().await?;
420	let weights = at_block.constants().entry(query)?;
421	let max_block = weights.per_class.normal.max_extrinsic.unwrap_or(weights.max_block);
422	Ok(max_block.0)
423}
424
425/// Get the automine status from the node.
426async fn get_automine(rpc_client: &RpcClient) -> bool {
427	match rpc_client.request::<bool>("getAutomine", rpc_params![]).await {
428		Ok(val) => val,
429		Err(err) => {
430			log::info!(target: LOG_TARGET, "Node does not have getAutomine RPC. Defaulting to automine=false. error: {err:?}");
431			false
432		},
433	}
434}
435
436/// Connect to a node at the given URL, and return the underlying API, RPC client, and legacy RPC
437/// clients.
438pub async fn connect(
439	node_rpc_url: &str,
440	max_request_size: u32,
441	max_response_size: u32,
442) -> Result<
443	(OnlineClient<SrcChainConfig>, RpcClient, LegacyRpcMethods<RpcConfigFor<SrcChainConfig>>),
444	ClientError,
445> {
446	log::info!(target: LOG_TARGET, "๐ŸŒ Connecting to node at: {node_rpc_url} ...");
447	let rpc_client = ReconnectingRpcClient::builder()
448		.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
449		.max_request_size(max_request_size)
450		.max_response_size(max_response_size)
451		.build(node_rpc_url.to_string())
452		.await?;
453	let rpc_client = RpcClient::new(rpc_client);
454	log::info!(target: LOG_TARGET, "๐ŸŒŸ Connected to node at: {node_rpc_url}");
455
456	// Pin the legacy backend explicitly. Since subxt 0.50, from_rpc_client defaults
457	// to the CombinedBackend, which routes block streams and header fetches through
458	// the chainHead protocol; its follow restarts and pinning limits stall receipt
459	// indexing under load (blocks become unresolvable once unpinned).
460	let backend = Arc::new(LegacyBackend::builder().build(rpc_client.clone()));
461	let api = OnlineClient::<SrcChainConfig>::from_backend(backend).await?;
462	let rpc = LegacyRpcMethods::<RpcConfigFor<SrcChainConfig>>::new(rpc_client.clone());
463	Ok((api, rpc_client, rpc))
464}
465
466impl Client {
467	/// Create a new client instance.
468	pub(crate) async fn new(
469		api: OnlineClient<SrcChainConfig>,
470		rpc_client: RpcClient,
471		rpc: LegacyRpcMethods<RpcConfigFor<SrcChainConfig>>,
472		block_provider: SubxtBlockInfoProvider,
473		receipt_provider: ReceiptProvider,
474		is_archive: bool,
475		subscription_gap_queue: SubscriptionGapQueue,
476		runtime_api_provider: VersionAwareRuntimeApiProvider,
477	) -> Result<Self, ClientError> {
478		let (chain_id, max_block_weight, automine) =
479			tokio::try_join!(chain_id(&api), max_block_weight(&api), async {
480				Ok(get_automine(&rpc_client).await)
481			},)?;
482
483		// Compute the very first capabilities so that the provider's cache has an anchor which
484		// the subscriptions grow forward and the backfill grows backward.
485		let latest_finalized_block = block_provider.latest_finalized_block().await;
486		runtime_api_provider.at(latest_finalized_block.block_hash()).await?;
487
488		// Fall back to 0 when the hardcoded value exceeds the current best block (e.g. zombienet
489		// reusing a known chain ID) and backward sync is disabled.
490		if !is_archive {
491			if let Some(known) = known_first_evm_block_for_chain(chain_id) {
492				let best = block_provider.latest_block_number().await;
493				if known > best {
494					log::debug!(
495						target: LOG_TARGET,
496						"Hardcoded first EVM block {known} exceeds best block {best} \
497						 for chain {chain_id}, defaulting to 0"
498					);
499					receipt_provider.set_first_evm_block(0).await?;
500				}
501			}
502		}
503
504		let client = Self {
505			api,
506			rpc_client,
507			rpc,
508			receipt_provider,
509			block_provider,
510			fee_history_provider: FeeHistoryProvider::default(),
511			chain_id,
512			max_block_weight,
513			automine,
514			block_notifier: automine
515				.then(|| tokio::sync::broadcast::channel::<H256>(NOTIFIER_CAPACITY).0),
516			subscription_lock: Arc::new(Mutex::new(())),
517			block_subscription_tx: tokio::sync::broadcast::channel(256).0,
518			log_subscription_tx: tokio::sync::broadcast::channel(1000).0,
519			is_archive,
520			backfill_complete: Arc::new(AtomicBool::new(false)),
521			subscription_gap_queue,
522			runtime_api_provider,
523		};
524
525		Ok(client)
526	}
527
528	/// Mark historic backfill as complete.
529	pub(crate) fn mark_backfill_complete(&self) {
530		self.backfill_complete.store(true, Ordering::Release);
531	}
532
533	/// Advance the sync_state head label if safe to do so.
534	/// Requires: archive mode, historic backfill complete, and no pending gap fills.
535	async fn advance_sync_head(&self, block_number: SubstrateBlockNumber, hash: H256) {
536		if !self.is_archive ||
537			!self.backfill_complete.load(Ordering::Acquire) ||
538			self.subscription_gap_queue.has_pending()
539		{
540			return;
541		}
542
543		if let Err(err) = self
544			.receipt_provider
545			.advance_sync_label(SyncLabel::Head, SyncCheckpoint::new(block_number, hash))
546			.await
547		{
548			log::warn!(target: LOG_TARGET, "Failed to advance sync head: {err:?}");
549		}
550	}
551
552	/// Creates a block notifier instance.
553	pub fn create_block_notifier(&mut self) {
554		self.block_notifier = Some(tokio::sync::broadcast::channel::<H256>(NOTIFIER_CAPACITY).0);
555	}
556
557	/// Sets a block notifier
558	pub fn set_block_notifier(&mut self, notifier: Option<tokio::sync::broadcast::Sender<H256>>) {
559		self.block_notifier = notifier;
560	}
561
562	pub(crate) fn api(&self) -> &OnlineClient<SrcChainConfig> {
563		&self.api
564	}
565
566	pub(crate) fn receipt_provider(&self) -> &ReceiptProvider {
567		&self.receipt_provider
568	}
569
570	pub(crate) fn block_provider(&self) -> &SubxtBlockInfoProvider {
571		&self.block_provider
572	}
573
574	pub(crate) fn subscription_gap_queue(&self) -> &SubscriptionGapQueue {
575		&self.subscription_gap_queue
576	}
577
578	/// The earliest block number where the ReviveApi is available.
579	/// Resolution order: in-memory value > known-networks table > 0.
580	fn earliest_block_number(&self) -> SubstrateBlockNumber {
581		self.receipt_provider
582			.first_evm_block()
583			.or_else(|| known_first_evm_block_for_chain(self.chain_id))
584			.unwrap_or(0)
585	}
586
587	/// Subscribe to new blocks, and execute the async closure for each block.
588	async fn subscribe_new_blocks<F, Fut>(
589		&self,
590		subscription_type: SubscriptionType,
591		callback: F,
592	) -> Result<(), ClientError>
593	where
594		F: Fn(SubstrateBlock) -> Fut + Send + Sync,
595		Fut: std::future::Future<Output = Result<(), ClientError>> + Send,
596	{
597		let mut block_stream = match subscription_type {
598			SubscriptionType::BestBlocks => self.api.stream_best_blocks().await,
599			SubscriptionType::FinalizedBlocks => self.api.stream_blocks().await,
600		}
601		.inspect_err(|err| {
602			log::error!(target: LOG_TARGET, "Failed to subscribe to blocks: {err:?}");
603		})?;
604
605		let mut last_finalized_seen: Option<SubstrateBlockNumber> = None;
606
607		while let Some(block) = block_stream.next().await {
608			let block = match block {
609				Ok(block) => block,
610				Err(err) => {
611					let err: subxt::Error = err.into();
612					if err.is_disconnected_will_reconnect() {
613						log::warn!(
614							target: LOG_TARGET,
615							"The RPC connection was lost and we may have missed a few blocks \
616							({subscription_type:?}, last finalized: {last_finalized_seen:?}): {err:?}"
617						);
618						continue;
619					}
620
621					log::error!(target: LOG_TARGET, "Failed to fetch block ({subscription_type:?}): {err:?}");
622					return Err(err.into());
623				},
624			};
625
626			// Resolution fails for pruned/retracted blocks and on transient RPC errors;
627			// erroring out here would kill the essential subscription task and with it the
628			// whole server. Skip instead: a skipped finalized block doesn't advance
629			// `last_finalized_seen`, so the gap filler backfills it.
630			let block = match block.at().await {
631				Ok(block) => block,
632				Err(err) => {
633					log::warn!(
634						target: LOG_TARGET,
635						"Failed to resolve streamed {subscription_type:?} block #{} ({:?}), skipping: {err:?}",
636						block.number(),
637						block.hash(),
638					);
639					continue;
640				},
641			};
642
643			// Acquire lock to ensure only one subscription can perform write operations at a time
644			let _guard = self.subscription_lock.lock().await;
645
646			let block_number = block.block_number();
647
648			// Only check finalized blocks for gaps.
649			if subscription_type == SubscriptionType::FinalizedBlocks {
650				if let Some(last) = last_finalized_seen {
651					self.subscription_gap_queue.detect_and_queue(block_number, last);
652				}
653				// Update unconditionally โ€” a callback failure doesn't mean the block was missed.
654				last_finalized_seen = Some(block_number);
655			}
656
657			log::trace!(target: LOG_TARGET_SUBSCRIPTION, "โณ Processing {subscription_type:?} block: {block_number}");
658			if let Err(err) = callback(block).await {
659				log::error!(target: LOG_TARGET, "Failed to process block {block_number}: {err:?}");
660			} else {
661				log::trace!(target: LOG_TARGET_SUBSCRIPTION, "โœ… Processed {subscription_type:?} block: {block_number}");
662			}
663		}
664
665		log::info!(target: LOG_TARGET, "Block subscription ended");
666		Ok(())
667	}
668
669	/// Extract receipts from a block, persist them and update fee history.
670	async fn process_block(
671		&self,
672		block: &SubstrateBlock,
673	) -> Result<(BlockV1, Vec<ReceiptInfo>), ClientError> {
674		let block_number = block.block_number();
675		let hash = block.block_hash();
676
677		macro_rules! time {
678			($label:expr, $expr:expr) => {{
679				let t = std::time::Instant::now();
680				let r = $expr;
681				log::trace!(
682					target: LOG_TARGET,
683					"โฑ๏ธ #{block_number} {}: {:?}",
684					$label, t.elapsed(),
685				);
686				r
687			}};
688		}
689
690		let eth_block = time!(
691			"eth_block",
692			self.runtime_api(hash)
693				.await?
694				.eth_block()
695				.ok_or(ClientError::UnsupportedRuntimeApiMethod("eth_block"))?
696				.await?
697		);
698		let receipts = time!(
699			"receipts_from_block",
700			self.receipt_provider.receipts_from_block(block, eth_block.hash).await?
701		);
702		time!(
703			"insert_block_receipts",
704			self.receipt_provider
705				.insert_block_receipts(block, &receipts, &eth_block.hash)
706				.await?
707		);
708
709		let (_, receipt_infos): (Vec<_>, Vec<_>) = receipts.into_iter().unzip();
710		self.fee_history_provider.update_fee_history(&eth_block, &receipt_infos).await;
711
712		Ok((eth_block, receipt_infos))
713	}
714
715	/// Start the block subscription, and populate the block cache.
716	pub async fn subscribe_and_cache_new_blocks(
717		&self,
718		subscription_type: SubscriptionType,
719	) -> Result<(), ClientError> {
720		log::info!(target: LOG_TARGET, "๐Ÿ”Œ Subscribing to new blocks ({subscription_type:?})");
721		self.subscribe_new_blocks(subscription_type, |block| async {
722			let hash = block.block_hash();
723
724			match subscription_type {
725				SubscriptionType::BestBlocks => {
726					let (eth_block, _) = self.process_block(&block).await?;
727					self.block_provider.update_latest(Arc::new(block), subscription_type).await;
728
729					if let Some(sender) = &self.block_notifier {
730						if sender.receiver_count() > 0 {
731							let _ = sender.send(hash);
732						}
733					}
734					if self.block_subscription_tx.receiver_count() > 0 {
735						let _ = self.block_subscription_tx.send(eth_block);
736					}
737				},
738				SubscriptionType::FinalizedBlocks => {
739					let block_number = block.block_number();
740					let (receipt_infos, eth_hash) = match self
741						.receipt_provider
742						.get_processed_eth_block_hash(block_number, hash)
743						.await
744					{
745						Some(eth_hash) => {
746							log::trace!(target: LOG_TARGET_SUBSCRIPTION,
747									"โฉ Finalized block #{block_number} already processed, \
748									 skipping extraction");
749							(None, eth_hash)
750						},
751						None => {
752							let (eth_block, infos) = self.process_block(&block).await?;
753							(Some(infos), eth_block.hash)
754						},
755					};
756
757					self.block_provider.update_latest(Arc::new(block), subscription_type).await;
758					self.advance_sync_head(block_number, hash).await;
759
760					if self.log_subscription_tx.receiver_count() > 0 {
761						let logs = match receipt_infos {
762							Some(infos) => infos.into_iter().flat_map(|r| r.logs).collect(),
763							None => {
764								self.receipt_provider
765									.logs_by_block_number(block_number, eth_hash)
766									.await?
767							},
768						};
769						for log in logs {
770							let _ = self.log_subscription_tx.send(log);
771						}
772					}
773				},
774			}
775
776			Ok(())
777		})
778		.await
779	}
780
781	/// Get the block hash for the given block number or tag.
782	pub async fn block_hash_for_tag(&self, at: BlockId) -> Result<SubstrateBlockHash, ClientError> {
783		match at {
784			BlockId::Hash(hash) => self
785				.resolve_substrate_hash(&H256::from(hash.block_hash.0))
786				.await
787				.ok_or(ClientError::EthereumBlockNotFound),
788			BlockId::Number(tag) => self
789				.block_by_number_or_tag(&tag)
790				.await?
791				.map(|block| block.block_hash())
792				.ok_or(ClientError::BlockNotFound),
793		}
794	}
795
796	/// Get a block for the specified hash or number.
797	pub async fn block_by_number_or_tag(
798		&self,
799		block: &BlockNumberOrTag,
800	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
801		match block {
802			BlockNumberOrTag::Number(n) => {
803				let n = (*n).try_into().map_err(|_| ClientError::ConversionFailed)?;
804				self.block_by_number(n).await
805			},
806			BlockNumberOrTag::Finalized | BlockNumberOrTag::Safe => {
807				let block = self.block_provider.latest_finalized_block().await;
808				Ok(Some(block))
809			},
810			BlockNumberOrTag::Earliest => self.block_by_number(self.earliest_block_number()).await,
811			BlockNumberOrTag::Latest | BlockNumberOrTag::Pending => {
812				let block = self.block_provider.latest_block().await;
813				Ok(Some(block))
814			},
815		}
816	}
817
818	/// Get the block for the given block number or tag, or `None` if it is not known.
819	///
820	/// Prefer this over resolving a hash with [`Self::block_hash_for_tag`] and then fetching the
821	/// block by that hash, which fetches the same block twice.
822	pub async fn block_for_tag(
823		&self,
824		at: BlockId,
825	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
826		match at {
827			BlockId::Hash(hash) => {
828				let Some(hash) = self.resolve_substrate_hash(&H256::from(hash.block_hash.0)).await
829				else {
830					return Ok(None);
831				};
832				self.block_by_hash(&hash).await
833			},
834			BlockId::Number(tag) => self.block_by_number_or_tag(&tag).await,
835		}
836	}
837
838	/// Resolve a [`BlockNumberOrTag`] to a concrete block number.
839	async fn resolve_tag_to_number(&self, tag: BlockNumberOrTag) -> Result<U256, ClientError> {
840		match tag {
841			BlockNumberOrTag::Number(n) => Ok(U256::from(n)),
842			BlockNumberOrTag::Earliest => Ok(U256::from(self.earliest_block_number())),
843			_ => Ok(self
844				.block_by_number_or_tag(&tag)
845				.await?
846				.ok_or(ClientError::BlockNotFound)?
847				.block_number()
848				.into()),
849		}
850	}
851
852	/// Get the storage API for the given block.
853	pub async fn storage_api(&self, block_hash: H256) -> Result<StorageApi, ClientError> {
854		Ok(StorageApi::new(self.api.at_block(block_hash).await?))
855	}
856
857	/// Get the version-aware runtime API for the given block.
858	pub async fn runtime_api(
859		&self,
860		block_hash: H256,
861	) -> Result<VersionAwareRuntimeApi, ClientError> {
862		self.runtime_api_provider.at(block_hash).await
863	}
864
865	/// Get the latest finalized block.
866	pub async fn latest_finalized_block(&self) -> Arc<SubstrateBlock> {
867		self.block_provider.latest_finalized_block().await
868	}
869
870	/// Get the latest best block.
871	pub async fn latest_block(&self) -> Arc<SubstrateBlock> {
872		self.block_provider.latest_block().await
873	}
874
875	/// Submit an ethereum transaction and return a stream of transaction status updates.
876	async fn submit_transaction(
877		&self,
878		call: subxt::tx::StaticPayload<EthTransact>,
879	) -> Result<StreamOfResults<TransactionStatus<SubstrateBlockHash>>, ClientError> {
880		let at_block = self.api.at_current_block().await?;
881		let ext = at_block.tx().create_unsigned(&call.unvalidated()).map_err(ClientError::from)?;
882
883		let sub = self
884			.rpc_client
885			.subscribe(
886				"author_submitAndWatchExtrinsic",
887				rpc_params![to_hex(ext.encoded())],
888				"author_unwatchExtrinsic",
889			)
890			.await?;
891
892		let sub = sub.map_err(|e| e.into());
893		Ok(StreamOf::new(Box::pin(sub)))
894	}
895
896	/// Expose the transaction API.
897	pub async fn submit(
898		&self,
899		call: subxt::tx::StaticPayload<EthTransact>,
900	) -> Result<TransactionStatus<SubstrateBlockHash>, ClientError> {
901		let mut progress = self.submit_transaction(call).await.inspect_err(|err| {
902			log::debug!(target: LOG_TARGET, "Failed to submit transaction: {err:?}");
903		})?;
904
905		tokio::time::timeout(Duration::from_secs(5), async {
906			if let Some(status) = progress.next().await {
907				match status {
908					Ok(
909						tx @ (TransactionStatus::Future |
910						TransactionStatus::Ready |
911						// Add other events that follow Ready here for completeness,
912						// but they can be ignored.
913						TransactionStatus::Broadcast(_) |
914						TransactionStatus::InBlock(_) |
915						TransactionStatus::FinalityTimeout(_) |
916						TransactionStatus::Retracted(_) |
917						TransactionStatus::Finalized(_)),
918					) => {
919						return Ok(tx);
920					},
921					Ok(
922						tx @ (TransactionStatus::Usurped(_) |
923						TransactionStatus::Dropped |
924						TransactionStatus::Invalid),
925					) => {
926						return Err(ClientError::SubmitError(tx.into()));
927					},
928					Err(err) => {
929						log::debug!(target: LOG_TARGET, "Transaction submission failed: {err:?}");
930						return Err(ClientError::from(err));
931					},
932				}
933			}
934			return Err(ClientError::SubmitError(SubmitError::StreamEnded));
935		})
936		.await
937		.map_err(|_| ClientError::Timeout)?
938	}
939
940	/// Get an EVM transaction receipt by hash.
941	pub async fn receipt(&self, tx_hash: &H256) -> Option<ReceiptInfo> {
942		self.receipt_provider.receipt_by_hash(tx_hash).await
943	}
944
945	/// Get all transaction receipts for the given block.
946	///
947	/// Returns `None` if the block does not exist.
948	pub async fn block_receipts(
949		&self,
950		at: BlockId,
951	) -> Result<Option<Vec<ReceiptInfo>>, ClientError> {
952		let Some(block) = self.block_for_tag(at).await? else {
953			return Ok(None);
954		};
955		let receipts = self.receipt_provider.block_receipts(&block).await?;
956		Ok(Some(receipts.into_iter().map(|(_, receipt)| receipt).collect()))
957	}
958
959	/// Get The post dispatch weight associated with this Ethereum transaction hash.
960	pub async fn post_dispatch_weight(&self, tx_hash: &H256) -> Option<Weight> {
961		use crate::subxt_client::system::events::ExtrinsicSuccess;
962		let ReceiptInfo { block_hash, transaction_index, .. } = self.receipt(tx_hash).await?;
963		let block_hash = self.resolve_substrate_hash(&block_hash).await?;
964		let block = self.block_provider.block_by_hash(&block_hash).await.ok()??;
965		let extrinsics = block.extrinsics().fetch().await.ok()?;
966		let ext = extrinsics.iter().nth(transaction_index.as_u32() as _)?.ok()?;
967		let event = ext.events().await.ok()?.find_first::<ExtrinsicSuccess>()?.ok()?;
968		Some(event.dispatch_info.weight.0)
969	}
970
971	pub async fn sync_state(
972		&self,
973	) -> Result<sc_rpc::system::SyncState<SubstrateBlockNumber>, ClientError> {
974		let client = self.rpc_client.clone();
975		let sync_state: sc_rpc::system::SyncState<SubstrateBlockNumber> =
976			client.request("system_syncState", Default::default()).await?;
977		Ok(sync_state)
978	}
979
980	/// Get the syncing status of the chain.
981	pub async fn syncing(&self) -> Result<SyncingStatus, ClientError> {
982		let health = self.rpc.system_health().await?;
983
984		let status = if health.is_syncing {
985			let sync_state = self.sync_state().await?;
986			SyncingStatus::SyncingProgress(SyncingProgress {
987				current_block: Some(sync_state.current_block.into()),
988				highest_block: Some(sync_state.highest_block.into()),
989				starting_block: Some(sync_state.starting_block.into()),
990			})
991		} else {
992			SyncingStatus::Bool(false)
993		};
994
995		Ok(status)
996	}
997
998	/// Get an EVM transaction receipt by hash.
999	pub async fn receipt_by_hash_and_index(
1000		&self,
1001		block_hash: &H256,
1002		transaction_index: usize,
1003	) -> Option<ReceiptInfo> {
1004		self.receipt_provider
1005			.receipt_by_block_hash_and_index(block_hash, transaction_index)
1006			.await
1007	}
1008
1009	pub async fn signed_tx_by_hash(&self, tx_hash: &H256) -> Option<TransactionSigned> {
1010		self.receipt_provider.signed_tx_by_hash(tx_hash).await
1011	}
1012
1013	/// Get receipts count per block.
1014	pub async fn receipts_count_per_block(&self, block_hash: &SubstrateBlockHash) -> Option<usize> {
1015		self.receipt_provider.receipts_count_per_block(block_hash).await
1016	}
1017
1018	/// Get an EVM transaction receipt by specified Ethereum block hash.
1019	pub async fn receipt_by_ethereum_hash_and_index(
1020		&self,
1021		ethereum_hash: &H256,
1022		transaction_index: usize,
1023	) -> Option<ReceiptInfo> {
1024		// Fallback: use hash as Substrate hash if Ethereum hash cannot be resolved
1025		let substrate_hash =
1026			self.resolve_substrate_hash(ethereum_hash).await.unwrap_or_else(|| {
1027				log::trace!(target: LOG_TARGET,
1028					"receipt_by_ethereum_hash_and_index: no ETH-to-substrate mapping for \
1029					 {ethereum_hash:?}, falling back to substrate hash lookup");
1030				*ethereum_hash
1031			});
1032		self.receipt_by_hash_and_index(&substrate_hash, transaction_index).await
1033	}
1034
1035	/// Get the system health.
1036	pub async fn system_health(&self) -> Result<SystemHealth, ClientError> {
1037		let health = self.rpc.system_health().await?;
1038		Ok(health)
1039	}
1040
1041	/// Get the block number of the latest block.
1042	pub async fn block_number(&self) -> Result<SubstrateBlockNumber, ClientError> {
1043		let latest_block = self.block_provider.latest_block().await;
1044		Ok(latest_block.block_number())
1045	}
1046
1047	/// Get a block by hash
1048	pub async fn block_by_hash(
1049		&self,
1050		hash: &SubstrateBlockHash,
1051	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
1052		self.block_provider.block_by_hash(hash).await
1053	}
1054
1055	/// Resolve Ethereum block hash to Substrate block hash, then get the block.
1056	/// This method provides the abstraction layer needed by the RPC APIs.
1057	pub async fn resolve_substrate_hash(&self, ethereum_hash: &H256) -> Option<H256> {
1058		self.receipt_provider.get_substrate_hash(ethereum_hash).await
1059	}
1060
1061	/// Resolve Substrate block hash to Ethereum block hash, then get the block.
1062	/// This method provides the abstraction layer needed by the RPC APIs.
1063	pub async fn resolve_ethereum_hash(&self, substrate_hash: &H256) -> Option<H256> {
1064		self.receipt_provider.get_ethereum_hash(substrate_hash).await
1065	}
1066
1067	/// Get a block by Ethereum hash with automatic resolution to Substrate hash.
1068	/// Falls back to treating the hash as a Substrate hash if no mapping exists.
1069	pub async fn block_by_ethereum_hash(
1070		&self,
1071		ethereum_hash: &H256,
1072	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
1073		// First try to resolve the Ethereum hash to a Substrate hash
1074		if let Some(substrate_hash) = self.resolve_substrate_hash(ethereum_hash).await {
1075			return self.block_by_hash(&substrate_hash).await;
1076		}
1077
1078		// Fallback: treat the provided hash as a Substrate hash (backward compatibility)
1079		log::trace!(target: LOG_TARGET,
1080			"block_by_ethereum_hash: no ETH-to-substrate mapping for {ethereum_hash:?}, \
1081			 falling back to substrate hash lookup");
1082		self.block_by_hash(ethereum_hash).await
1083	}
1084
1085	/// Get a block by number
1086	pub async fn block_by_number(
1087		&self,
1088		block_number: SubstrateBlockNumber,
1089	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
1090		self.block_provider.block_by_number(block_number).await
1091	}
1092
1093	/// Get a block hash for the given block number.
1094	pub async fn get_block_hash(
1095		&self,
1096		block_number: SubstrateBlockNumber,
1097	) -> Result<Option<SubstrateBlockHash>, ClientError> {
1098		let maybe_block = self.block_provider.block_by_number(block_number).await?;
1099		Ok(maybe_block.map(|block| block.block_hash()))
1100	}
1101
1102	async fn tracing_block(
1103		&self,
1104		block_hash: H256,
1105	) -> Result<
1106		sp_runtime::generic::Block<
1107			sp_runtime::generic::Header<u32, sp_runtime::traits::BlakeTwo256>,
1108			sp_runtime::OpaqueExtrinsic,
1109		>,
1110		ClientError,
1111	> {
1112		let signed_block: Option<
1113			sp_runtime::generic::SignedBlock<
1114				sp_runtime::generic::Block<
1115					sp_runtime::generic::Header<u32, sp_runtime::traits::BlakeTwo256>,
1116					sp_runtime::OpaqueExtrinsic,
1117				>,
1118			>,
1119		> = self.rpc_client.request("chain_getBlock", rpc_params![block_hash]).await?;
1120
1121		Ok(signed_block.ok_or(ClientError::BlockNotFound)?.block)
1122	}
1123
1124	/// Get the transaction traces for the given block.
1125	pub async fn trace_block_by_number(
1126		&self,
1127		at: BlockNumberOrTag,
1128		config: TracerTypeV1,
1129	) -> Result<Vec<TransactionTrace>, ClientError> {
1130		if self.receipt_provider.is_before_earliest_block(&at) {
1131			return Ok(vec![]);
1132		}
1133
1134		let block_hash = self.block_hash_for_tag(at.into()).await?;
1135		let block = self.tracing_block(block_hash).await?;
1136		let parent_hash = block.header().parent_hash;
1137		// Block 0 has no parent โ€” there is nothing to trace.
1138		if parent_hash == Default::default() {
1139			return Ok(vec![]);
1140		}
1141
1142		let CallRecordedOutput { value: trace_entries, degraded } = self
1143			.runtime_api(parent_hash)
1144			.await?
1145			.trace_block(block, config, block_hash)
1146			.ok_or(ClientError::UnsupportedRuntimeApiMethod("trace_block"))?
1147			.await?;
1148
1149		let mut hashes = self
1150			.receipt_provider
1151			.block_transaction_hashes(&block_hash)
1152			.await
1153			.ok_or(ClientError::EthExtrinsicNotFound)?;
1154
1155		let mut entries = trace_entries
1156			.into_iter()
1157			.filter_map(|(index, entry)| {
1158				let index = index as usize;
1159				let tx_hash = hashes.remove(&index)?;
1160				let outcome = match entry {
1161					TraceEntry::Traced(trace) => TraceOutcome::Trace(trace),
1162					TraceEntry::NotTraced => {
1163						TraceOutcome::Error(ClientError::TraceUnavailable.to_string())
1164					},
1165				};
1166				Some((index, TransactionTrace { tx_hash, outcome }))
1167			})
1168			.collect::<Vec<_>>();
1169
1170		if degraded {
1171			entries.extend(hashes.into_iter().map(|(index, tx_hash)| {
1172				let outcome = TraceOutcome::Error(ClientError::TraceUnavailable.to_string());
1173				(index, TransactionTrace { tx_hash, outcome })
1174			}));
1175			entries.sort_unstable_by_key(|(index, _)| *index);
1176		}
1177
1178		Ok(entries.into_iter().map(|(_, entry)| entry).collect())
1179	}
1180
1181	/// Get the transaction traces for the given transaction.
1182	pub async fn trace_transaction(
1183		&self,
1184		transaction_hash: H256,
1185		config: TracerTypeV1,
1186	) -> Result<TraceV1, ClientError> {
1187		let (block_hash, transaction_index) = self
1188			.receipt_provider
1189			.find_transaction(&transaction_hash)
1190			.await
1191			.ok_or(ClientError::EthExtrinsicNotFound)?;
1192		let transaction_index = transaction_index as u32;
1193
1194		let block = self.tracing_block(block_hash).await?;
1195		let parent_hash = block.header.parent_hash;
1196		let CallRecordedOutput { value: entry, degraded } = self
1197			.runtime_api(parent_hash)
1198			.await?
1199			.trace_tx(block, transaction_index, config, block_hash)
1200			.ok_or(ClientError::UnsupportedRuntimeApiMethod("trace_tx"))?
1201			.await?;
1202
1203		match entry {
1204			Some(TraceEntry::Traced(trace)) => Ok(trace),
1205			Some(TraceEntry::NotTraced) => Err(ClientError::TraceUnavailable),
1206			None if degraded => Err(ClientError::TraceUnavailable),
1207			None => Err(ClientError::EthExtrinsicNotFound),
1208		}
1209	}
1210
1211	/// Get the transaction traces for the given block.
1212	pub async fn trace_call(
1213		&self,
1214		transaction: GenericTransactionV1,
1215		block: BlockId,
1216		config: TracerTypeV1,
1217		state_overrides: Option<StateOverrideSetV1>,
1218	) -> Result<TraceV1, ClientError> {
1219		let block_hash = self.block_hash_for_tag(block).await?;
1220		let runtime_api = self.runtime_api(block_hash).await?;
1221		runtime_api
1222			.trace_call(transaction, config, state_overrides)
1223			.ok_or(ClientError::UnsupportedRuntimeApiMethod("trace_call"))?
1224			.await
1225	}
1226
1227	/// Get the EVM block for the given Substrate block.
1228	pub async fn evm_block(
1229		&self,
1230		block: Arc<SubstrateBlock>,
1231		hydrated_transactions: bool,
1232	) -> Option<BlockV1> {
1233		log::trace!(target: LOG_TARGET, "Get Ethereum block for hash {:?}", block.block_hash());
1234
1235		if self
1236			.receipt_provider
1237			.is_before_earliest_block(&BlockNumberOrTag::Number(block.block_number().into()))
1238		{
1239			log::trace!(target: LOG_TARGET,
1240				"Block #{} is before receipt floor, skipping", block.block_number());
1241			return None;
1242		}
1243
1244		// This could potentially fail under below circumstances:
1245		//  - state has been pruned
1246		//  - the block author cannot be obtained from the digest logs (highly unlikely)
1247		//  - the node we are targeting has an outdated revive pallet (or ETH block functionality is
1248		//    disabled)
1249		let eth_block_result = async {
1250			self.runtime_api(block.block_hash())
1251				.await?
1252				.eth_block()
1253				.ok_or(ClientError::UnsupportedRuntimeApiMethod("eth_block"))?
1254				.await
1255		}
1256		.await;
1257		match eth_block_result {
1258			Ok(mut eth_block) => {
1259				log::trace!(target: LOG_TARGET, "Ethereum block from runtime API hash {:?}", eth_block.hash);
1260
1261				if hydrated_transactions {
1262					// Hydrate the block.
1263					let tx_infos = self
1264						.receipt_provider
1265						.receipts_from_block(&block, eth_block.hash)
1266						.await
1267						.inspect_err(|err| {
1268							log::trace!(target: LOG_TARGET,
1269								"Failed to extract receipts for block #{}: {err:?}",
1270								block.block_number());
1271						})
1272						.unwrap_or_default()
1273						.into_iter()
1274						.map(|(signed_tx, receipt)| receipt.transaction_info(signed_tx))
1275						.collect::<Vec<_>>();
1276
1277					eth_block.transactions = HashesOrTransactionInfosV1::TransactionInfos(tx_infos);
1278				}
1279
1280				Some(eth_block)
1281			},
1282			Err(err) => {
1283				log::error!(target: LOG_TARGET, "Failed to get Ethereum block for hash {:?}: {err:?}", block.block_hash());
1284				None
1285			},
1286		}
1287	}
1288
1289	/// Get the chain ID.
1290	pub fn chain_id(&self) -> u64 {
1291		self.chain_id
1292	}
1293
1294	/// Get the Max Block Weight.
1295	pub fn max_block_weight(&self) -> Weight {
1296		self.max_block_weight
1297	}
1298
1299	/// Get the block notifier, if automine is enabled or Self::create_block_notifier was called.
1300	pub fn block_notifier(&self) -> Option<tokio::sync::broadcast::Sender<H256>> {
1301		self.block_notifier.clone()
1302	}
1303
1304	/// Get the logs matching the given filter.
1305	pub async fn logs(&self, filter: Option<Filter>) -> Result<Vec<Log>, ClientError> {
1306		let logs =
1307			self.receipt_provider
1308				.logs(filter, |tag| async move {
1309					self.resolve_tag_to_number(tag).await.map_err(Into::into)
1310				})
1311				.await
1312				.map_err(ClientError::LogFilterFailed)?;
1313
1314		Ok(logs)
1315	}
1316
1317	pub async fn fee_history(
1318		&self,
1319		block_count: u32,
1320		latest_block: BlockNumberOrTag,
1321		reward_percentiles: Option<Vec<f64>>,
1322	) -> Result<FeeHistoryResult, ClientError> {
1323		let Some(latest_block) = self.block_by_number_or_tag(&latest_block).await? else {
1324			return Err(ClientError::BlockNotFound);
1325		};
1326
1327		self.fee_history_provider
1328			.fee_history(block_count, latest_block.block_number(), reward_percentiles)
1329			.await
1330	}
1331
1332	/// Check if automine is enabled.
1333	pub fn is_automine(&self) -> bool {
1334		self.automine
1335	}
1336
1337	/// Get the automine status from the node.
1338	pub async fn get_automine(&self) -> bool {
1339		get_automine(&self.rpc_client).await
1340	}
1341
1342	/// Gets the block subscription rx side of the channel.
1343	pub fn get_block_subscription_rx(&self) -> tokio::sync::broadcast::Receiver<BlockV1> {
1344		self.block_subscription_tx.subscribe()
1345	}
1346
1347	/// Gets the log subscription rx side of the channel.
1348	pub fn get_log_subscription_rx(&self) -> tokio::sync::broadcast::Receiver<Log> {
1349		self.log_subscription_tx.subscribe()
1350	}
1351}
1352
1353fn to_hex(bytes: impl AsRef<[u8]>) -> String {
1354	format!("0x{}", hex::encode(bytes.as_ref()))
1355}
1356
1357#[cfg(test)]
1358mod tests {
1359	use super::*;
1360	use sc_rpc_api::state::error::{
1361		CALL_RECORDED_DENIED_ERROR_CODE, CALL_RECORDED_UNSUPPORTED_ERROR_CODE,
1362	};
1363	use subxt::rpcs::UserError;
1364
1365	fn rpc_user_error(code: i32) -> ClientError {
1366		ClientError::RpcError(subxt::rpcs::Error::User(UserError {
1367			code,
1368			message: "..".to_string(),
1369			data: None,
1370		}))
1371	}
1372
1373	#[test]
1374	fn missing_method_is_version_skew() {
1375		assert_eq!(
1376			rpc_user_error(-32601).recorded_unavailable_reason(),
1377			Some(RecordedUnavailable::MethodMissing),
1378		);
1379	}
1380
1381	#[test]
1382	fn unsafe_denied_is_a_fallback_reason() {
1383		assert_eq!(
1384			rpc_user_error(CALL_RECORDED_DENIED_ERROR_CODE).recorded_unavailable_reason(),
1385			Some(RecordedUnavailable::Denied),
1386		);
1387	}
1388
1389	#[test]
1390	fn no_recorder_code_is_expected() {
1391		assert_eq!(
1392			rpc_user_error(CALL_RECORDED_UNSUPPORTED_ERROR_CODE).recorded_unavailable_reason(),
1393			Some(RecordedUnavailable::NoRecorder),
1394		);
1395	}
1396
1397	#[test]
1398	fn genuine_errors_do_not_trigger_fallback() {
1399		assert!(rpc_user_error(-32000).recorded_unavailable_reason().is_none());
1400		assert!(rpc_user_error(-32602).recorded_unavailable_reason().is_none());
1401		assert!(ClientError::EthExtrinsicNotFound.recorded_unavailable_reason().is_none());
1402		assert!(ClientError::BlockNotFound.recorded_unavailable_reason().is_none());
1403	}
1404}