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