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