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	subxt_client::{self, revive::calls::types::EthTransact, SrcChainConfig},
25	BlockInfoProvider, BlockTag, FeeHistoryProvider, ReceiptProvider, SubxtBlockInfoProvider,
26	TracerType, TransactionInfo,
27};
28use jsonrpsee::types::{error::CALL_EXECUTION_FAILED_CODE, ErrorObjectOwned};
29use pallet_revive::{
30	evm::{
31		decode_revert_reason, Block, BlockNumberOrTag, BlockNumberOrTagOrHash, FeeHistoryResult,
32		Filter, GenericTransaction, HashesOrTransactionInfos, Log, ReceiptInfo, SyncingProgress,
33		SyncingStatus, Trace, TransactionSigned, TransactionTrace, H256,
34	},
35	EthTransactError,
36};
37use runtime_api::RuntimeApi;
38use sp_runtime::traits::Block as BlockT;
39use sp_weights::Weight;
40use std::{ops::Range, sync::Arc, time::Duration};
41use storage_api::StorageApi;
42use subxt::{
43	backend::{
44		legacy::{rpc_methods::SystemHealth, LegacyRpcMethods},
45		rpc::{
46			reconnecting_rpc_client::{ExponentialBackoff, RpcClient as ReconnectingRpcClient},
47			RpcClient,
48		},
49	},
50	config::{HashFor, Header},
51	ext::subxt_rpcs::rpc_params,
52	Config, OnlineClient,
53};
54use thiserror::Error;
55use tokio::sync::Mutex;
56
57/// The substrate block type.
58pub type SubstrateBlock = subxt::blocks::Block<SrcChainConfig, OnlineClient<SrcChainConfig>>;
59
60/// The substrate block header.
61pub type SubstrateBlockHeader = <SrcChainConfig as Config>::Header;
62
63/// The substrate block number type.
64pub type SubstrateBlockNumber = <SubstrateBlockHeader as Header>::Number;
65
66/// The substrate block hash type.
67pub type SubstrateBlockHash = HashFor<SrcChainConfig>;
68
69/// The runtime balance type.
70pub type Balance = u128;
71
72/// The subscription type used to listen to new blocks.
73#[derive(Debug, Clone, Copy, PartialEq)]
74pub enum SubscriptionType {
75	/// Subscribe to best blocks.
76	BestBlocks,
77	/// Subscribe to finalized blocks.
78	FinalizedBlocks,
79}
80
81/// The error type for the client.
82#[derive(Error, Debug)]
83pub enum ClientError {
84	/// A [`jsonrpsee::core::ClientError`] wrapper error.
85	#[error(transparent)]
86	Jsonrpsee(#[from] jsonrpsee::core::ClientError),
87	/// A [`subxt::Error`] wrapper error.
88	#[error(transparent)]
89	SubxtError(#[from] subxt::Error),
90	#[error(transparent)]
91	RpcError(#[from] subxt::ext::subxt_rpcs::Error),
92	/// A [`sqlx::Error`] wrapper error.
93	#[error(transparent)]
94	SqlxError(#[from] sqlx::Error),
95	/// A [`codec::Error`] wrapper error.
96	#[error(transparent)]
97	CodecError(#[from] codec::Error),
98	/// Transcact call failed.
99	#[error("contract reverted: {0:?}")]
100	TransactError(EthTransactError),
101	/// A decimal conversion failed.
102	#[error("conversion failed")]
103	ConversionFailed,
104	/// The block hash was not found.
105	#[error("hash not found")]
106	BlockNotFound,
107	/// The contract was not found.
108	#[error("Contract not found")]
109	ContractNotFound,
110	#[error("No Ethereum extrinsic found")]
111	EthExtrinsicNotFound,
112	/// The transaction fee could not be found
113	#[error("transactionFeePaid event not found")]
114	TxFeeNotFound,
115	/// Failed to decode a raw payload into a signed transaction.
116	#[error("Failed to decode a raw payload into a signed transaction")]
117	TxDecodingFailed,
118	/// Failed to recover eth address.
119	#[error("failed to recover eth address")]
120	RecoverEthAddressFailed,
121	/// Failed to filter logs.
122	#[error("Failed to filter logs")]
123	LogFilterFailed(#[from] anyhow::Error),
124	/// Receipt storage was not found.
125	#[error("Receipt storage not found")]
126	ReceiptDataNotFound,
127	/// Ethereum block was not found.
128	#[error("Ethereum block not found")]
129	EthereumBlockNotFound,
130	/// Receipt data length mismatch.
131	#[error("Receipt data length mismatch")]
132	ReceiptDataLengthMismatch,
133}
134const LOG_TARGET: &str = "eth-rpc::client";
135
136const REVERT_CODE: i32 = 3;
137
138const NOTIFIER_CAPACITY: usize = 16;
139impl From<ClientError> for ErrorObjectOwned {
140	fn from(err: ClientError) -> Self {
141		match err {
142			ClientError::SubxtError(subxt::Error::Rpc(subxt::error::RpcError::ClientError(
143				subxt::ext::subxt_rpcs::Error::User(err),
144			))) |
145			ClientError::RpcError(subxt::ext::subxt_rpcs::Error::User(err)) =>
146				ErrorObjectOwned::owned::<Vec<u8>>(err.code, err.message, None),
147			ClientError::TransactError(EthTransactError::Data(data)) => {
148				let msg = match decode_revert_reason(&data) {
149					Some(reason) => format!("execution reverted: {reason}"),
150					None => "execution reverted".to_string(),
151				};
152
153				let data = format!("0x{}", hex::encode(data));
154				ErrorObjectOwned::owned::<String>(REVERT_CODE, msg, Some(data))
155			},
156			ClientError::TransactError(EthTransactError::Message(msg)) =>
157				ErrorObjectOwned::owned::<String>(CALL_EXECUTION_FAILED_CODE, msg, None),
158			_ =>
159				ErrorObjectOwned::owned::<String>(CALL_EXECUTION_FAILED_CODE, err.to_string(), None),
160		}
161	}
162}
163
164/// A client connect to a node and maintains a cache of the last `CACHE_SIZE` blocks.
165#[derive(Clone)]
166pub struct Client {
167	api: OnlineClient<SrcChainConfig>,
168	rpc_client: RpcClient,
169	rpc: LegacyRpcMethods<SrcChainConfig>,
170	receipt_provider: ReceiptProvider,
171	block_provider: SubxtBlockInfoProvider,
172	fee_history_provider: FeeHistoryProvider,
173	chain_id: u64,
174	max_block_weight: Weight,
175	/// Whether the node has automine enabled.
176	automine: bool,
177	/// A notifier, that informs subscribers of new best blocks.
178	block_notifier: Option<tokio::sync::broadcast::Sender<H256>>,
179	/// A lock to ensure only one subscription can perform write operations at a time.
180	subscription_lock: Arc<Mutex<()>>,
181}
182
183/// Fetch the chain ID from the substrate chain.
184async fn chain_id(api: &OnlineClient<SrcChainConfig>) -> Result<u64, ClientError> {
185	let query = subxt_client::constants().revive().chain_id();
186	api.constants().at(&query).map_err(|err| err.into())
187}
188
189/// Fetch the max block weight from the substrate chain.
190async fn max_block_weight(api: &OnlineClient<SrcChainConfig>) -> Result<Weight, ClientError> {
191	let query = subxt_client::constants().system().block_weights();
192	let weights = api.constants().at(&query)?;
193	let max_block = weights.per_class.normal.max_extrinsic.unwrap_or(weights.max_block);
194	Ok(max_block.0)
195}
196
197/// Get the automine status from the node.
198async fn get_automine(rpc_client: &RpcClient) -> bool {
199	match rpc_client.request::<bool>("getAutomine", rpc_params![]).await {
200		Ok(val) => val,
201		Err(err) => {
202			log::info!(target: LOG_TARGET, "Node does not have getAutomine RPC. Defaulting to automine=false. error: {err:?}");
203			false
204		},
205	}
206}
207
208/// Connect to a node at the given URL, and return the underlying API, RPC client, and legacy RPC
209/// clients.
210pub async fn connect(
211	node_rpc_url: &str,
212) -> Result<(OnlineClient<SrcChainConfig>, RpcClient, LegacyRpcMethods<SrcChainConfig>), ClientError>
213{
214	log::info!(target: LOG_TARGET, "๐ŸŒ Connecting to node at: {node_rpc_url} ...");
215	let rpc_client = ReconnectingRpcClient::builder()
216		.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
217		.build(node_rpc_url.to_string())
218		.await?;
219	let rpc_client = RpcClient::new(rpc_client);
220	log::info!(target: LOG_TARGET, "๐ŸŒŸ Connected to node at: {node_rpc_url}");
221
222	let api = OnlineClient::<SrcChainConfig>::from_rpc_client(rpc_client.clone()).await?;
223	let rpc = LegacyRpcMethods::<SrcChainConfig>::new(rpc_client.clone());
224	Ok((api, rpc_client, rpc))
225}
226
227impl Client {
228	/// Create a new client instance.
229	pub async fn new(
230		api: OnlineClient<SrcChainConfig>,
231		rpc_client: RpcClient,
232		rpc: LegacyRpcMethods<SrcChainConfig>,
233		block_provider: SubxtBlockInfoProvider,
234		receipt_provider: ReceiptProvider,
235	) -> Result<Self, ClientError> {
236		let (chain_id, max_block_weight, automine) =
237			tokio::try_join!(chain_id(&api), max_block_weight(&api), async {
238				Ok(get_automine(&rpc_client).await)
239			},)?;
240
241		let client = Self {
242			api,
243			rpc_client,
244			rpc,
245			receipt_provider,
246			block_provider,
247			fee_history_provider: FeeHistoryProvider::default(),
248			chain_id,
249			max_block_weight,
250			automine,
251			block_notifier: automine
252				.then(|| tokio::sync::broadcast::channel::<H256>(NOTIFIER_CAPACITY).0),
253			subscription_lock: Arc::new(Mutex::new(())),
254		};
255
256		Ok(client)
257	}
258
259	/// Creates a block notifier instance.
260	pub fn create_block_notifier(&mut self) {
261		self.block_notifier = Some(tokio::sync::broadcast::channel::<H256>(NOTIFIER_CAPACITY).0);
262	}
263
264	/// Sets a block notifier
265	pub fn set_block_notifier(&mut self, notifier: Option<tokio::sync::broadcast::Sender<H256>>) {
266		self.block_notifier = notifier;
267	}
268
269	/// Subscribe to past blocks executing the callback for each block in `range`.
270	async fn subscribe_past_blocks<F, Fut>(
271		&self,
272		range: Range<SubstrateBlockNumber>,
273		callback: F,
274	) -> Result<(), ClientError>
275	where
276		F: Fn(Arc<SubstrateBlock>) -> Fut + Send + Sync,
277		Fut: std::future::Future<Output = Result<(), ClientError>> + Send,
278	{
279		let mut block = self
280			.block_provider
281			.block_by_number(range.end)
282			.await?
283			.ok_or(ClientError::BlockNotFound)?;
284
285		loop {
286			let block_number = block.number();
287			log::trace!(target: "eth-rpc::subscription", "Processing past block #{block_number}");
288
289			let parent_hash = block.header().parent_hash;
290			callback(block.clone()).await.inspect_err(|err| {
291				log::error!(target: "eth-rpc::subscription", "Failed to process past block #{block_number}: {err:?}");
292			})?;
293
294			if range.start < block_number {
295				block = self
296					.block_provider
297					.block_by_hash(&parent_hash)
298					.await?
299					.ok_or(ClientError::BlockNotFound)?;
300			} else {
301				return Ok(());
302			}
303		}
304	}
305
306	/// Subscribe to new blocks, and execute the async closure for each block.
307	async fn subscribe_new_blocks<F, Fut>(
308		&self,
309		subscription_type: SubscriptionType,
310		callback: F,
311	) -> Result<(), ClientError>
312	where
313		F: Fn(SubstrateBlock) -> Fut + Send + Sync,
314		Fut: std::future::Future<Output = Result<(), ClientError>> + Send,
315	{
316		let mut block_stream = match subscription_type {
317			SubscriptionType::BestBlocks => self.api.blocks().subscribe_best().await,
318			SubscriptionType::FinalizedBlocks => self.api.blocks().subscribe_finalized().await,
319		}
320		.inspect_err(|err| {
321			log::error!(target: LOG_TARGET, "Failed to subscribe to blocks: {err:?}");
322		})?;
323
324		while let Some(block) = block_stream.next().await {
325			let block = match block {
326				Ok(block) => block,
327				Err(err) => {
328					if err.is_disconnected_will_reconnect() {
329						log::warn!(
330							target: LOG_TARGET,
331							"The RPC connection was lost and we may have missed a few blocks ({subscription_type:?}): {err:?}"
332						);
333						continue;
334					}
335
336					log::error!(target: LOG_TARGET, "Failed to fetch block ({subscription_type:?}): {err:?}");
337					return Err(err.into());
338				},
339			};
340
341			// Acquire lock to ensure only one subscription can perform write operations at a time
342			let _guard = self.subscription_lock.lock().await;
343
344			let block_number = block.number();
345			log::trace!(target: "eth-rpc::subscription", "โณ Processing {subscription_type:?} block: {block_number}");
346			if let Err(err) = callback(block).await {
347				log::error!(target: LOG_TARGET, "Failed to process block {block_number}: {err:?}");
348			} else {
349				log::trace!(target: "eth-rpc::subscription", "โœ… Processed {subscription_type:?} block: {block_number}");
350			}
351		}
352
353		log::info!(target: LOG_TARGET, "Block subscription ended");
354		Ok(())
355	}
356
357	/// Start the block subscription, and populate the block cache.
358	pub async fn subscribe_and_cache_new_blocks(
359		&self,
360		subscription_type: SubscriptionType,
361	) -> Result<(), ClientError> {
362		log::info!(target: LOG_TARGET, "๐Ÿ”Œ Subscribing to new blocks ({subscription_type:?})");
363		self.subscribe_new_blocks(subscription_type, |block| async {
364			let hash = block.hash();
365			let evm_block = self.runtime_api(hash).eth_block().await?;
366			let (_, receipts): (Vec<_>, Vec<_>) = self
367				.receipt_provider
368				.insert_block_receipts(&block, &evm_block.hash)
369				.await?
370				.into_iter()
371				.unzip();
372
373			self.block_provider.update_latest(Arc::new(block), subscription_type).await;
374			self.fee_history_provider.update_fee_history(&evm_block, &receipts).await;
375
376			// Only broadcast for best blocks to avoid duplicate notifications.
377			match (subscription_type, &self.block_notifier) {
378				(SubscriptionType::BestBlocks, Some(sender)) if sender.receiver_count() > 0 => {
379					let _ = sender.send(hash);
380				},
381				_ => {},
382			}
383			Ok(())
384		})
385		.await
386	}
387
388	/// Cache old blocks up to the given block number.
389	pub async fn subscribe_and_cache_blocks(
390		&self,
391		index_last_n_blocks: SubstrateBlockNumber,
392	) -> Result<(), ClientError> {
393		let last = self.latest_block().await.number().saturating_sub(1);
394		let range = last.saturating_sub(index_last_n_blocks)..last;
395		log::info!(target: LOG_TARGET, "๐Ÿ—„๏ธ Indexing past blocks in range {range:?}");
396
397		self.subscribe_past_blocks(range, |block| async move {
398			let ethereum_hash = self
399				.runtime_api(block.hash())
400				.eth_block_hash(pallet_revive::evm::U256::from(block.number()))
401				.await?
402				.ok_or(ClientError::EthereumBlockNotFound)?;
403			self.receipt_provider.insert_block_receipts(&block, &ethereum_hash).await?;
404			Ok(())
405		})
406		.await?;
407
408		log::info!(target: LOG_TARGET, "๐Ÿ—„๏ธ Finished indexing past blocks");
409		Ok(())
410	}
411
412	/// Get the block hash for the given block number or tag.
413	pub async fn block_hash_for_tag(
414		&self,
415		at: BlockNumberOrTagOrHash,
416	) -> Result<SubstrateBlockHash, ClientError> {
417		match at {
418			BlockNumberOrTagOrHash::BlockHash(hash) => self
419				.resolve_substrate_hash(&hash)
420				.await
421				.ok_or(ClientError::EthereumBlockNotFound),
422			BlockNumberOrTagOrHash::BlockNumber(block_number) => {
423				let n: SubstrateBlockNumber =
424					(block_number).try_into().map_err(|_| ClientError::ConversionFailed)?;
425				let hash = self.get_block_hash(n).await?.ok_or(ClientError::BlockNotFound)?;
426				Ok(hash)
427			},
428			BlockNumberOrTagOrHash::BlockTag(BlockTag::Finalized | BlockTag::Safe) => {
429				let block = self.latest_finalized_block().await;
430				Ok(block.hash())
431			},
432			BlockNumberOrTagOrHash::BlockTag(_) => {
433				let block = self.latest_block().await;
434				Ok(block.hash())
435			},
436		}
437	}
438
439	/// Get the storage API for the given block.
440	pub fn storage_api(&self, block_hash: H256) -> StorageApi {
441		StorageApi::new(self.api.storage().at(block_hash))
442	}
443
444	/// Get the runtime API for the given block.
445	pub fn runtime_api(&self, block_hash: H256) -> RuntimeApi {
446		RuntimeApi::new(self.api.runtime_api().at(block_hash))
447	}
448
449	/// Get the latest finalized block.
450	pub async fn latest_finalized_block(&self) -> Arc<SubstrateBlock> {
451		self.block_provider.latest_finalized_block().await
452	}
453
454	/// Get the latest best block.
455	pub async fn latest_block(&self) -> Arc<SubstrateBlock> {
456		self.block_provider.latest_block().await
457	}
458
459	/// Expose the transaction API.
460	pub async fn submit(
461		&self,
462		call: subxt::tx::DefaultPayload<EthTransact>,
463	) -> Result<(), ClientError> {
464		let ext = self.api.tx().create_unsigned(&call).map_err(ClientError::from)?;
465		let hash: H256 = self
466			.rpc_client
467			.request("author_submitExtrinsic", rpc_params![to_hex(ext.encoded())])
468			.await?;
469		log::debug!(target: LOG_TARGET, "Submitted transaction with substrate hash: {hash:?}");
470		Ok(())
471	}
472
473	/// Get an EVM transaction receipt by hash.
474	pub async fn receipt(&self, tx_hash: &H256) -> Option<ReceiptInfo> {
475		self.receipt_provider.receipt_by_hash(tx_hash).await
476	}
477
478	/// Get The post dispatch weight associated with this Ethereum transaction hash.
479	pub async fn post_dispatch_weight(&self, tx_hash: &H256) -> Option<Weight> {
480		use crate::subxt_client::system::events::ExtrinsicSuccess;
481		let ReceiptInfo { block_hash, transaction_index, .. } = self.receipt(tx_hash).await?;
482		let block_hash = self.resolve_substrate_hash(&block_hash).await?;
483		let block = self.block_provider.block_by_hash(&block_hash).await.ok()??;
484		let ext = block.extrinsics().await.ok()?.iter().nth(transaction_index.as_u32() as _)?;
485		let event = ext.events().await.ok()?.find_first::<ExtrinsicSuccess>().ok()??;
486		Some(event.dispatch_info.weight.0)
487	}
488
489	pub async fn sync_state(
490		&self,
491	) -> Result<sc_rpc::system::SyncState<SubstrateBlockNumber>, ClientError> {
492		let client = self.rpc_client.clone();
493		let sync_state: sc_rpc::system::SyncState<SubstrateBlockNumber> =
494			client.request("system_syncState", Default::default()).await?;
495		Ok(sync_state)
496	}
497
498	/// Get the syncing status of the chain.
499	pub async fn syncing(&self) -> Result<SyncingStatus, ClientError> {
500		let health = self.rpc.system_health().await?;
501
502		let status = if health.is_syncing {
503			let sync_state = self.sync_state().await?;
504			SyncingProgress {
505				current_block: Some(sync_state.current_block.into()),
506				highest_block: Some(sync_state.highest_block.into()),
507				starting_block: Some(sync_state.starting_block.into()),
508			}
509			.into()
510		} else {
511			SyncingStatus::Bool(false)
512		};
513
514		Ok(status)
515	}
516
517	/// Get an EVM transaction receipt by hash.
518	pub async fn receipt_by_hash_and_index(
519		&self,
520		block_hash: &H256,
521		transaction_index: usize,
522	) -> Option<ReceiptInfo> {
523		self.receipt_provider
524			.receipt_by_block_hash_and_index(block_hash, transaction_index)
525			.await
526	}
527
528	pub async fn signed_tx_by_hash(&self, tx_hash: &H256) -> Option<TransactionSigned> {
529		self.receipt_provider.signed_tx_by_hash(tx_hash).await
530	}
531
532	/// Get receipts count per block.
533	pub async fn receipts_count_per_block(&self, block_hash: &SubstrateBlockHash) -> Option<usize> {
534		self.receipt_provider.receipts_count_per_block(block_hash).await
535	}
536
537	/// Get an EVM transaction receipt by specified Ethereum block hash.
538	pub async fn receipt_by_ethereum_hash_and_index(
539		&self,
540		ethereum_hash: &H256,
541		transaction_index: usize,
542	) -> Option<ReceiptInfo> {
543		// Fallback: use hash as Substrate hash if Ethereum hash cannot be resolved
544		let substrate_hash =
545			self.resolve_substrate_hash(ethereum_hash).await.unwrap_or(*ethereum_hash);
546		self.receipt_by_hash_and_index(&substrate_hash, transaction_index).await
547	}
548
549	/// Get the system health.
550	pub async fn system_health(&self) -> Result<SystemHealth, ClientError> {
551		let health = self.rpc.system_health().await?;
552		Ok(health)
553	}
554
555	/// Get the block number of the latest block.
556	pub async fn block_number(&self) -> Result<SubstrateBlockNumber, ClientError> {
557		let latest_block = self.block_provider.latest_block().await;
558		Ok(latest_block.number())
559	}
560
561	/// Get a block hash for the given block number.
562	pub async fn get_block_hash(
563		&self,
564		block_number: SubstrateBlockNumber,
565	) -> Result<Option<SubstrateBlockHash>, ClientError> {
566		let maybe_block = self.block_provider.block_by_number(block_number).await?;
567		Ok(maybe_block.map(|block| block.hash()))
568	}
569
570	/// Get a block for the specified hash or number.
571	pub async fn block_by_number_or_tag(
572		&self,
573		block: &BlockNumberOrTag,
574	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
575		match block {
576			BlockNumberOrTag::U256(n) => {
577				let n = (*n).try_into().map_err(|_| ClientError::ConversionFailed)?;
578				self.block_by_number(n).await
579			},
580			BlockNumberOrTag::BlockTag(BlockTag::Finalized | BlockTag::Safe) => {
581				let block = self.block_provider.latest_finalized_block().await;
582				Ok(Some(block))
583			},
584			BlockNumberOrTag::BlockTag(_) => {
585				let block = self.block_provider.latest_block().await;
586				Ok(Some(block))
587			},
588		}
589	}
590
591	/// Get a block by hash
592	pub async fn block_by_hash(
593		&self,
594		hash: &SubstrateBlockHash,
595	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
596		self.block_provider.block_by_hash(hash).await
597	}
598
599	/// Resolve Ethereum block hash to Substrate block hash, then get the block.
600	/// This method provides the abstraction layer needed by the RPC APIs.
601	pub async fn resolve_substrate_hash(&self, ethereum_hash: &H256) -> Option<H256> {
602		self.receipt_provider.get_substrate_hash(ethereum_hash).await
603	}
604
605	/// Resolve Substrate block hash to Ethereum block hash, then get the block.
606	/// This method provides the abstraction layer needed by the RPC APIs.
607	pub async fn resolve_ethereum_hash(&self, substrate_hash: &H256) -> Option<H256> {
608		self.receipt_provider.get_ethereum_hash(substrate_hash).await
609	}
610
611	/// Get a block by Ethereum hash with automatic resolution to Substrate hash.
612	/// Falls back to treating the hash as a Substrate hash if no mapping exists.
613	pub async fn block_by_ethereum_hash(
614		&self,
615		ethereum_hash: &H256,
616	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
617		// First try to resolve the Ethereum hash to a Substrate hash
618		if let Some(substrate_hash) = self.resolve_substrate_hash(ethereum_hash).await {
619			return self.block_by_hash(&substrate_hash).await;
620		}
621
622		// Fallback: treat the provided hash as a Substrate hash (backward compatibility)
623		self.block_by_hash(ethereum_hash).await
624	}
625
626	/// Get a block by number
627	pub async fn block_by_number(
628		&self,
629		block_number: SubstrateBlockNumber,
630	) -> Result<Option<Arc<SubstrateBlock>>, ClientError> {
631		self.block_provider.block_by_number(block_number).await
632	}
633
634	async fn tracing_block(
635		&self,
636		block_hash: H256,
637	) -> Result<
638		sp_runtime::generic::Block<
639			sp_runtime::generic::Header<u32, sp_runtime::traits::BlakeTwo256>,
640			sp_runtime::OpaqueExtrinsic,
641		>,
642		ClientError,
643	> {
644		let signed_block: sp_runtime::generic::SignedBlock<
645			sp_runtime::generic::Block<
646				sp_runtime::generic::Header<u32, sp_runtime::traits::BlakeTwo256>,
647				sp_runtime::OpaqueExtrinsic,
648			>,
649		> = self
650			.rpc_client
651			.request("chain_getBlock", rpc_params![block_hash])
652			.await
653			.unwrap();
654
655		Ok(signed_block.block)
656	}
657
658	/// Get the transaction traces for the given block.
659	pub async fn trace_block_by_number(
660		&self,
661		at: BlockNumberOrTag,
662		config: TracerType,
663	) -> Result<Vec<TransactionTrace>, ClientError> {
664		if self.receipt_provider.is_before_earliest_block(&at) {
665			return Ok(vec![]);
666		}
667
668		let block_hash = self.block_hash_for_tag(at.into()).await?;
669		let block = self.tracing_block(block_hash).await?;
670		let parent_hash = block.header().parent_hash;
671		let runtime_api = RuntimeApi::new(self.api.runtime_api().at(parent_hash));
672		let traces = runtime_api.trace_block(block, config.clone()).await?;
673
674		let mut hashes = self
675			.receipt_provider
676			.block_transaction_hashes(&block_hash)
677			.await
678			.ok_or(ClientError::EthExtrinsicNotFound)?;
679
680		let traces = traces.into_iter().filter_map(|(index, trace)| {
681			Some(TransactionTrace { tx_hash: hashes.remove(&(index as usize))?, trace })
682		});
683
684		Ok(traces.collect())
685	}
686
687	/// Get the transaction traces for the given transaction.
688	pub async fn trace_transaction(
689		&self,
690		transaction_hash: H256,
691		config: TracerType,
692	) -> Result<Trace, ClientError> {
693		let (block_hash, transaction_index) = self
694			.receipt_provider
695			.find_transaction(&transaction_hash)
696			.await
697			.ok_or(ClientError::EthExtrinsicNotFound)?;
698
699		let block = self.tracing_block(block_hash).await?;
700		let parent_hash = block.header.parent_hash;
701		let runtime_api = self.runtime_api(parent_hash);
702
703		runtime_api.trace_tx(block, transaction_index as u32, config).await
704	}
705
706	/// Get the transaction traces for the given block.
707	pub async fn trace_call(
708		&self,
709		transaction: GenericTransaction,
710		block: BlockNumberOrTagOrHash,
711		config: TracerType,
712	) -> Result<Trace, ClientError> {
713		let block_hash = self.block_hash_for_tag(block).await?;
714		let runtime_api = self.runtime_api(block_hash);
715		runtime_api.trace_call(transaction, config).await
716	}
717
718	/// Get the EVM block for the given Substrate block.
719	pub async fn evm_block(
720		&self,
721		block: Arc<SubstrateBlock>,
722		hydrated_transactions: bool,
723	) -> Option<Block> {
724		log::trace!(target: LOG_TARGET, "Get Ethereum block for hash {:?}", block.hash());
725
726		// This could potentially fail under below circumstances:
727		//  - state has been pruned
728		//  - the block author cannot be obtained from the digest logs (highly unlikely)
729		//  - the node we are targeting has an outdated revive pallet (or ETH block functionality is
730		//    disabled)
731		match self.runtime_api(block.hash()).eth_block().await {
732			Ok(mut eth_block) => {
733				log::trace!(target: LOG_TARGET, "Ethereum block from runtime API hash {:?}", eth_block.hash);
734
735				if hydrated_transactions {
736					// Hydrate the block.
737					let tx_infos = self
738						.receipt_provider
739						.receipts_from_block(&block)
740						.await
741						.unwrap_or_default()
742						.into_iter()
743						.map(|(signed_tx, receipt)| TransactionInfo::new(&receipt, signed_tx))
744						.collect::<Vec<_>>();
745
746					eth_block.transactions = HashesOrTransactionInfos::TransactionInfos(tx_infos);
747				}
748
749				Some(eth_block)
750			},
751			Err(err) => {
752				log::error!(target: LOG_TARGET, "Failed to get Ethereum block for hash {:?}: {err:?}", block.hash());
753				None
754			},
755		}
756	}
757
758	/// Get the chain ID.
759	pub fn chain_id(&self) -> u64 {
760		self.chain_id
761	}
762
763	/// Get the Max Block Weight.
764	pub fn max_block_weight(&self) -> Weight {
765		self.max_block_weight
766	}
767
768	/// Get the block notifier, if automine is enabled or Self::create_block_notifier was called.
769	pub fn block_notifier(&self) -> Option<tokio::sync::broadcast::Sender<H256>> {
770		self.block_notifier.clone()
771	}
772
773	/// Get the logs matching the given filter.
774	pub async fn logs(&self, filter: Option<Filter>) -> Result<Vec<Log>, ClientError> {
775		let logs =
776			self.receipt_provider.logs(filter).await.map_err(ClientError::LogFilterFailed)?;
777		Ok(logs)
778	}
779
780	pub async fn fee_history(
781		&self,
782		block_count: u32,
783		latest_block: BlockNumberOrTag,
784		reward_percentiles: Option<Vec<f64>>,
785	) -> Result<FeeHistoryResult, ClientError> {
786		let Some(latest_block) = self.block_by_number_or_tag(&latest_block).await? else {
787			return Err(ClientError::BlockNotFound);
788		};
789
790		self.fee_history_provider
791			.fee_history(block_count, latest_block.number(), reward_percentiles)
792			.await
793	}
794
795	/// Check if automine is enabled.
796	pub fn is_automine(&self) -> bool {
797		self.automine
798	}
799
800	/// Get the automine status from the node.
801	pub async fn get_automine(&self) -> bool {
802		get_automine(&self.rpc_client).await
803	}
804}
805
806fn to_hex(bytes: impl AsRef<[u8]>) -> String {
807	format!("0x{}", hex::encode(bytes.as_ref()))
808}