Repository: paritytech/polkadot-sdk (language: rust)

Issue:
[pallet-revive] tx fee fixes
Apply some fixes to properly estimate ethereum tx fees:

- Set the `extension_weight` on the dispatch_info to properly calculate the fee with pallet_transaction_payment
- Expose the gas_price through Runtime API, just in case we decide to tweak the value in future updates, it should be read from the chain rather than be a shared constant exposed by the crate
- add a `evm_gas_to_fee` utility function to properly convert gas to substrate fee
- Fix some minor gas encoding for edge cases


Discussion / hints:

Fix patch:
diff --git a/prdoc/pr_7482.prdoc b/prdoc/pr_7482.prdoc
new file mode 100644
index 0000000000000..0f548582a21a8
--- /dev/null
+++ b/prdoc/pr_7482.prdoc
@@ -0,0 +1,7 @@
+title: '[pallet-revive] rpc - gas used fixes '
+doc:
+- audience: Runtime Dev
+  description: '#7463 follow up with RPC fixes'
+crates:
+- name: pallet-revive-eth-rpc
+  bump: minor
diff --git a/substrate/frame/revive/rpc/examples/rust/deploy.rs b/substrate/frame/revive/rpc/examples/rust/deploy.rs
index b74d7ea18d41f..12f539bba42ff 100644
--- a/substrate/frame/revive/rpc/examples/rust/deploy.rs
+++ b/substrate/frame/revive/rpc/examples/rust/deploy.rs
@@ -19,10 +19,8 @@ use pallet_revive::{
 	create1,
 	evm::{Account, BlockTag, ReceiptInfo, U256},
 };
-use pallet_revive_eth_rpc::{
-	example::{wait_for_receipt, TransactionBuilder},
-	EthRpcClient,
-};
+use pallet_revive_eth_rpc::{example::TransactionBuilder, EthRpcClient};
+use std::sync::Arc;
 
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
@@ -36,43 +34,49 @@ async fn main() -> anyhow::Result<()> {
 	println!("Account:");
 	println!("- address: {:?}", account.address());
 	println!("- substrate: {}", account.substrate_account());
-	let client = HttpClientBuilder::default().build("http://localhost:8545")?;
+	let client = Arc::new(HttpClientBuilder::default().build("http://localhost:8545")?);
 
 	println!("\n\n=== Deploying contract ===\n\n");
 
 	let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?;
-	let hash = TransactionBuilder::default()
+	let tx = TransactionBuilder::new(&client)
 		.value(5_000_000_000_000u128.into())
 		.input(input)
-		.send(&client)
+		.send()
 		.await?;
 
-	println!("Deploy Tx hash: {hash:?}");
+	println!("Deploy Tx hash: {:?}", tx.hash());
 	let ReceiptInfo { block_number, gas_used, contract_address, .. } =
-		wait_for_receipt(&client, hash).await?;
+		tx.wait_for_receipt().await?;
 
 	let contract_address = contract_address.unwrap();
 	assert_eq!(contract_address, create1(&account.address(), nonce.try_into().unwrap()));
 
 	println!("Receipt:");
-	println!("- Block number: {block_number}");
-	println!("- Gas used: {gas_used}");
+	println!("- Block number:     {block_number}");
+	println!("- Gas estimated:    {}", tx.gas());
+	println!("- Gas used:         {gas_used}");
 	println!("- Contract address: {contract_address:?}");
 	let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?;
 	println!("- Contract balance: {balance:?}");
 
+	if std::env::var("SKIP_CALL").is_ok() {
+		return Ok(());
+	}
+
 	println!("\n\n=== Calling contract ===\n\n");
-	let hash = TransactionBuilder::default()
+	let tx = TransactionBuilder::new(&client)
 		.value(U256::from(1_000_000u32))
 		.to(contract_address)
-		.send(&client)
+		.send()
 		.await?;
 
-	println!("Contract call tx hash: {hash:?}");
-	let ReceiptInfo { block_number, gas_used, to, .. } = wait_for_receipt(&client, hash).await?;
+	println!("Contract call tx hash: {:?}", tx.hash());
+	let ReceiptInfo { block_number, gas_used, to, .. } = tx.wait_for_receipt().await?;
 	println!("Receipt:");
-	println!("- Block number: {block_number}");
-	println!("- Gas used: {gas_used}");
-	println!("- To: {to:?}");
+	println!("- Block number:  {block_number}");
+	println!("- Gas used:      {gas_used}");
+	println!("- Gas estimated: {}", tx.gas());
+	println!("- To:            {to:?}");
 	Ok(())
 }
diff --git a/substrate/frame/revive/rpc/examples/rust/rpc-playground.rs b/substrate/frame/revive/rpc/examples/rust/rpc-playground.rs
index 64175ca60b5f4..ea8916b3b993d 100644
--- a/substrate/frame/revive/rpc/examples/rust/rpc-playground.rs
+++ b/substrate/frame/revive/rpc/examples/rust/rpc-playground.rs
@@ -17,13 +17,14 @@
 use jsonrpsee::http_client::HttpClientBuilder;
 use pallet_revive::evm::{Account, BlockTag};
 use pallet_revive_eth_rpc::EthRpcClient;
+use std::sync::Arc;
 
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
 	let account = Account::default();
 	println!("Account address: {:?}", account.address());
 
-	let client = HttpClientBuilder::default().build("http://localhost:8545")?;
+	let client = Arc::new(HttpClientBuilder::default().build("http://localhost:8545")?);
 
 	let block = client.get_block_by_number(BlockTag::Latest.into(), false).await?;
 	println!("Latest block: {block:#?}");
diff --git a/substrate/frame/revive/rpc/examples/rust/transfer.rs b/substrate/frame/revive/rpc/examples/rust/transfer.rs
index 1d67a2dba28f9..5f16716760f9a 100644
--- a/substrate/frame/revive/rpc/examples/rust/transfer.rs
+++ b/substrate/frame/revive/rpc/examples/rust/transfer.rs
@@ -16,14 +16,12 @@
 // limitations under the License.
 use jsonrpsee::http_client::HttpClientBuilder;
 use pallet_revive::evm::{Account, BlockTag, ReceiptInfo};
-use pallet_revive_eth_rpc::{
-	example::{wait_for_receipt, TransactionBuilder},
-	EthRpcClient,
-};
+use pallet_revive_eth_rpc::{example::TransactionBuilder, EthRpcClient};
+use std::sync::Arc;
 
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
-	let client = HttpClientBuilder::default().build("http://localhost:8545")?;
+	let client = Arc::new(HttpClientBuilder::default().build("http://localhost:8545")?);
 
 	let alith = Account::default();
 	let alith_address = alith.address();
@@ -41,16 +39,15 @@ async fn main() -> anyhow::Result<()> {
 	print_balance().await?;
 	println!("\n\n=== Transferring  ===\n\n");
 
-	let hash = TransactionBuilder::default()
+	let tx = TransactionBuilder::new(&client)
 		.signer(alith)
 		.value(value)
 		.to(ethan.address())
-		.send(&client)
+		.send()
 		.await?;
-	println!("Transaction hash: {hash:?}");
+	println!("Transaction hash: {:?}", tx.hash());
 
-	let ReceiptInfo { block_number, gas_used, status, .. } =
-		wait_for_receipt(&client, hash).await?;
+	let ReceiptInfo { block_number, gas_used, status, .. } = tx.wait_for_receipt().await?;
 	println!("Receipt: ");
 	println!("- Block number: {block_number}");
 	println!("- Gas used: {gas_used}");
diff --git a/substrate/frame/revive/rpc/revive_chain.metadata b/substrate/frame/revive/rpc/revive_chain.metadata
index 29d91486afe00..89476924cf007 100644
Binary files a/substrate/frame/revive/rpc/revive_chain.metadata and b/substrate/frame/revive/rpc/revive_chain.metadata differ
diff --git a/substrate/frame/revive/rpc/src/cli.rs b/substrate/frame/revive/rpc/src/cli.rs
index b6c57d2c3b0bf..7ebf53e7fbfe0 100644
--- a/substrate/frame/revive/rpc/src/cli.rs
+++ b/substrate/frame/revive/rpc/src/cli.rs
@@ -16,9 +16,9 @@
 // limitations under the License.
 //! The Ethereum JSON-RPC server.
 use crate::{
-	client::{connect, Client},
+	client::{connect, native_to_eth_ratio, Client},
 	BlockInfoProvider, BlockInfoProviderImpl, CacheReceiptProvider, DBReceiptProvider,
-	EthRpcServer, EthRpcServerImpl, ReceiptProvider, SystemHealthRpcServer,
+	EthRpcServer, EthRpcServerImpl, ReceiptExtractor, ReceiptProvider, SystemHealthRpcServer,
 	SystemHealthRpcServerImpl, LOG_TARGET,
 };
 use clap::Parser;
@@ -146,6 +146,8 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
 			let (api, rpc_client, rpc) = connect(&node_rpc_url).await?;
 			let block_provider: Arc<dyn BlockInfoProvider> =
 				Arc::new(BlockInfoProviderImpl::new(cache_size, api.clone(), rpc.clone()));
+
+			let receipt_extractor = ReceiptExtractor::new(native_to_eth_ratio(&api).await?);
 			let receipt_provider: Arc<dyn ReceiptProvider> =
 				if let Some(database_url) = database_url.as_ref() {
 					log::info!(target: LOG_TARGET, "🔗 Connecting to provided database");
@@ -155,6 +157,7 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
 							database_url,
 							database_read_only,
 							block_provider.clone(),
+							receipt_extractor.clone(),
 						)
 						.await?,
 					))
@@ -163,8 +166,15 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
 					Arc::new(CacheReceiptProvider::default())
 				};
 
-			let client =
-				Client::new(api, rpc_client, rpc, block_provider, receipt_provider).await?;
+			let client = Client::new(
+				api,
+				rpc_client,
+				rpc,
+				block_provider,
+				receipt_provider,
+				receipt_extractor,
+			)
+			.await?;
 			client.subscribe_and_cache_blocks(&essential_spawn_handle);
 			Ok::<_, crate::ClientError>(client)
 		}
diff --git a/substrate/frame/revive/rpc/src/client.rs b/substrate/frame/revive/rpc/src/client.rs
index 7f45a27a05a35..e312114e90e30 100644
--- a/substrate/frame/revive/rpc/src/client.rs
+++ b/substrate/frame/revive/rpc/src/client.rs
@@ -17,11 +17,10 @@
 //! The client connects to the source substrate chain
 //! and is used by the rpc server to query and send transactions to the substrate chain.
 use crate::{
-	extract_receipts_from_block,
 	subxt_client::{
 		revive::calls::types::EthTransact, runtime_types::pallet_revive::storage::ContractInfo,
 	},
-	BlockInfoProvider, ReceiptProvider, TransactionInfo, LOG_TARGET,
+	BlockInfoProvider, ReceiptExtractor, ReceiptProvider, TransactionInfo, LOG_TARGET,
 };
 use jsonrpsee::types::{error::CALL_EXECUTION_FAILED_CODE, ErrorObjectOwned};
 use pallet_revive::{
@@ -170,6 +169,7 @@ pub struct Client {
 	rpc: LegacyRpcMethods<SrcChainConfig>,
 	receipt_provider: Arc<dyn ReceiptProvider>,
 	block_provider: Arc<dyn BlockInfoProvider>,
+	receipt_extractor: ReceiptExtractor,
 	chain_id: u64,
 	max_block_weight: Weight,
 }
@@ -180,6 +180,12 @@ async fn chain_id(api: &OnlineClient<SrcChainConfig>) -> Result<u64, ClientError
 	api.constants().at(&query).map_err(|err| err.into())
 }
 
+/// Fetch the native_to_eth_ratio
+pub async fn native_to_eth_ratio(api: &OnlineClient<SrcChainConfig>) -> Result<u32, ClientError> {
+	let query = subxt_client::constants().revive().native_to_eth_ratio();
+	api.constants().at(&query).map_err(|err| err.into())
+}
+
 /// Fetch the max block weight from the substrate chain.
 async fn max_block_weight(api: &OnlineClient<SrcChainConfig>) -> Result<Weight, ClientError> {
 	let query = subxt_client::constants().system().block_weights();
@@ -226,6 +232,7 @@ impl Client {
 		rpc: LegacyRpcMethods<SrcChainConfig>,
 		block_provider: Arc<dyn BlockInfoProvider>,
 		receipt_provider: Arc<dyn ReceiptProvider>,
+		receipt_extractor: ReceiptExtractor,
 	) -> Result<Self, ClientError> {
 		let (chain_id, max_block_weight) =
 			tokio::try_join!(chain_id(&api), max_block_weight(&api))?;
@@ -236,6 +243,7 @@ impl Client {
 			rpc,
 			receipt_provider,
 			block_provider,
+			receipt_extractor,
 			chain_id,
 			max_block_weight,
 		})
@@ -320,7 +328,7 @@ impl Client {
 				},
 			};
 
-			log::debug!(target: LOG_TARGET, "Pushing block: {}", block.number());
+			log::trace!(target: LOG_TARGET, "Pushing block: {}", block.number());
 			if let Err(err) = callback(block).await {
 				log::error!(target: LOG_TARGET, "Failed to process block: {err:?}");
 			}
@@ -336,7 +344,7 @@ impl Client {
 		spawn_handle.spawn("subscribe-blocks", None, async move {
 			let res = client
 				.subscribe_new_blocks(SubscriptionType::BestBlocks, |block| async {
-					let receipts = extract_receipts_from_block(&block).await?;
+					let receipts = client.receipt_extractor.extract_from_block(&block).await?;
 
 					client.receipt_provider.insert(&block.hash(), &receipts).await;
 					if let Some(pruned) = client.block_provider.cache_block(block).await {
@@ -360,9 +368,10 @@ impl Client {
 	) -> Result<(), ClientError> {
 		let new_blocks_fut =
 			self.subscribe_new_blocks(SubscriptionType::FinalizedBlocks, |block| async move {
-				let receipts = extract_receipts_from_block(&block).await.inspect_err(|err| {
-					log::error!(target: LOG_TARGET, "Failed to extract receipts from block: {err:?}");
-				})?;
+				let receipts =
+					self.receipt_extractor.extract_from_block(&block).await.inspect_err(|err| {
+						log::error!(target: LOG_TARGET, "Failed to extract receipts from block: {err:?}");
+					})?;
 				self.receipt_provider.insert(&block.hash(), &receipts).await;
 				Ok(())
 			});
@@ -370,7 +379,7 @@ impl Client {
 		let Some(oldest_block) = oldest_block else { return new_blocks_fut.await };
 
 		let old_blocks_fut = self.subscribe_past_blocks(|block| async move {
-			let receipts = extract_receipts_from_block(&block).await?;
+			let receipts = self.receipt_extractor.extract_from_block(&block).await?;
 			self.receipt_provider.insert(&block.hash(), &receipts).await;
 			if block.number() == oldest_block {
 				Ok(ControlFlow::Break(()))
@@ -481,7 +490,7 @@ impl Client {
 	pub async fn receipt_by_hash_and_index(
 		&self,
 		block_hash: &H256,
-		transaction_index: &U256,
+		transaction_index: usize,
 	) -> Option<ReceiptInfo> {
 		self.receipt_provider
 			.receipt_by_block_hash_and_index(block_hash, transaction_index)
@@ -665,7 +674,7 @@ impl Client {
 		let state_root = header.state_root.0.into();
 		let extrinsics_root = header.extrinsics_root.0.into();
 
-		let receipts = extract_receipts_from_block(&block).await.unwrap_or_default();
+		let receipts = self.receipt_extractor.extract_from_block(&block).await.unwrap_or_default();
 		let gas_used =
 			receipts.iter().fold(U256::zero(), |acc, (_, receipt)| acc + receipt.gas_used);
 		let transactions = if hydrated_transactions {
diff --git a/substrate/frame/revive/rpc/src/eth-indexer.rs b/substrate/frame/revive/rpc/src/eth-indexer.rs
index 894143be0a525..6e3dc0b4bc5cf 100644
--- a/substrate/frame/revive/rpc/src/eth-indexer.rs
+++ b/substrate/frame/revive/rpc/src/eth-indexer.rs
@@ -17,8 +17,8 @@
 //! The Ethereum JSON-RPC server.
 use clap::Parser;
 use pallet_revive_eth_rpc::{
-	client::{connect, Client, SubstrateBlockNumber},
-	BlockInfoProvider, BlockInfoProviderImpl, DBReceiptProvider, ReceiptProvider,
+	client::{connect, native_to_eth_ratio, Client, SubstrateBlockNumber},
+	BlockInfoProvider, BlockInfoProviderImpl, DBReceiptProvider, ReceiptExtractor, ReceiptProvider,
 };
 use sc_cli::SharedParams;
 use std::sync::Arc;
@@ -78,10 +78,20 @@ pub async fn main() -> anyhow::Result<()> {
 	let (api, rpc_client, rpc) = connect(&node_rpc_url).await?;
 	let block_provider: Arc<dyn BlockInfoProvider> =
 		Arc::new(BlockInfoProviderImpl::new(0, api.clone(), rpc.clone()));
-	let receipt_provider: Arc<dyn ReceiptProvider> =
-		Arc::new(DBReceiptProvider::new(&database_url, false, block_provider.clone()).await?);
+	let receipt_extractor = ReceiptExtractor::new(native_to_eth_ratio(&api).await?);
+	let receipt_provider: Arc<dyn ReceiptProvider> = Arc::new(
+		DBReceiptProvider::new(
+			&database_url,
+			false,
+			block_provider.clone(),
+			receipt_extractor.clone(),
+		)
+		.await?,
+	);
 
-	let client = Client::new(api, rpc_client, rpc, block_provider, receipt_provider).await?;
+	let client =
+		Client::new(api, rpc_client, rpc, block_provider, receipt_provider, receipt_extractor)
+			.await?;
 	client.subscribe_and_cache_receipts(oldest_block).await?;
 
 	Ok(())
diff --git a/substrate/frame/revive/rpc/src/example.rs b/substrate/frame/revive/rpc/src/example.rs
index aad5b4fbc344d..c8c633a4e982c 100644
--- a/substrate/frame/revive/rpc/src/example.rs
+++ b/substrate/frame/revive/rpc/src/example.rs
@@ -20,38 +20,11 @@ use anyhow::Context;
 use pallet_revive::evm::{
 	Account, BlockTag, Bytes, GenericTransaction, TransactionLegacyUnsigned, H160, H256, U256,
 };
-
-/// Wait for a transaction receipt.
-pub async fn wait_for_receipt(
-	client: &(impl EthRpcClient + Send + Sync),
-	hash: H256,
-) -> anyhow::Result<ReceiptInfo> {
-	for _ in 0..30 {
-		tokio::time::sleep(std::time::Duration::from_secs(2)).await;
-		let receipt = client.get_transaction_receipt(hash).await?;
-		if let Some(receipt) = receipt {
-			return Ok(receipt)
-		}
-	}
-
-	anyhow::bail!("Failed to get receipt")
-}
-
-/// Wait for a successful transaction receipt.
-pub async fn wait_for_successful_receipt(
-	client: &(impl EthRpcClient + Send + Sync),
-	hash: H256,
-) -> anyhow::Result<ReceiptInfo> {
-	let receipt = wait_for_receipt(client, hash).await?;
-	if receipt.is_success() {
-		Ok(receipt)
-	} else {
-		anyhow::bail!("Transaction failed")
-	}
-}
+use std::sync::Arc;
 
 /// Transaction builder.
-pub struct TransactionBuilder {
+pub struct TransactionBuilder<Client: EthRpcClient + Sync + Send> {
+	client: Arc<Client>,
 	signer: Account,
 	value: U256,
 	input: Bytes,
@@ -59,9 +32,51 @@ pub struct TransactionBuilder {
 	mutate: Box<dyn FnOnce(&mut TransactionLegacyUnsigned)>,
 }
 
-impl Default for TransactionBuilder {
-	fn default() -> Self {
+#[derive(Debug)]
+pub struct SubmittedTransaction<Client: EthRpcClient + Sync + Send> {
+	tx: GenericTransaction,
+	hash: H256,
+	client: Arc<Client>,
+}
+
+impl<Client: EthRpcClient + Sync + Send> SubmittedTransaction<Client> {
+	/// Get the hash of the transaction.
+	pub fn hash(&self) -> H256 {
+		self.hash
+	}
+
+	/// The gas sent with the transaction.
+	pub fn gas(&self) -> U256 {
+		self.tx.gas.unwrap()
+	}
+
+	/// Wait for the receipt of the transaction.
+	pub async fn wait_for_receipt(&self) -> anyhow::Result<ReceiptInfo> {
+		let hash = self.hash();
+		for _ in 0..30 {
+			tokio::time::sleep(std::time::Duration::from_secs(2)).await;
+			let receipt = self.client.get_transaction_receipt(hash).await?;
+			if let Some(receipt) = receipt {
+				if receipt.is_success() {
+					assert!(
+						self.gas() > receipt.gas_used,
+						"Gas used should be less than gas estimated."
+					);
+					return Ok(receipt)
+				} else {
+					anyhow::bail!("Transaction failed")
+				}
+			}
+		}
+
+		anyhow::bail!("Timeout, failed to get receipt")
+	}
+}
+
+impl<Client: EthRpcClient + Send + Sync> TransactionBuilder<Client> {
+	pub fn new(client: &Arc<Client>) -> Self {
 		Self {
+			client: Arc::clone(client),
 			signer: Account::default(),
 			value: U256::zero(),
 			input: Bytes::default(),
@@ -69,9 +84,6 @@ impl Default for TransactionBuilder {
 			mutate: Box::new(|_| {}),
 		}
 	}
-}
-
-impl TransactionBuilder {
 	/// Set the signer.
 	pub fn signer(mut self, signer: Account) -> Self {
 		self.signer = signer;
@@ -103,11 +115,8 @@ impl TransactionBuilder {
 	}
 
 	/// Call eth_call to get the result of a view function
-	pub async fn eth_call(
-		self,
-		client: &(impl EthRpcClient + Send + Sync),
-	) -> anyhow::Result<Vec<u8>> {
-		let TransactionBuilder { signer, value, input, to, .. } = self;
+	pub async fn eth_call(self) -> anyhow::Result<Vec<u8>> {
+		let TransactionBuilder { client, signer, value, input, to, .. } = self;
 
 		let from = signer.address();
 		let result = client
@@ -127,8 +136,8 @@ impl TransactionBuilder {
 	}
 
 	/// Send the transaction.
-	pub async fn send(self, client: &(impl EthRpcClient + Send + Sync)) -> anyhow::Result<H256> {
-		let TransactionBuilder { signer, value, input, to, mutate } = self;
+	pub async fn send(self) -> anyhow::Result<SubmittedTransaction<Client>> {
+		let TransactionBuilder { client, signer, value, input, to, mutate } = self;
 
 		let from = signer.address();
 		let chain_id = Some(client.chain_id().await?);
@@ -153,6 +162,7 @@ impl TransactionBuilder {
 			.await
 			.with_context(|| "Failed to fetch gas estimate")?;
 
+		println!("Gas estimate: {gas:?}");
 		let mut unsigned_tx = TransactionLegacyUnsigned {
 			gas,
 			nonce,
@@ -166,23 +176,18 @@ impl TransactionBuilder {
 
 		mutate(&mut unsigned_tx);
 
-		let tx = signer.sign_transaction(unsigned_tx.into());
-		let bytes = tx.signed_payload();
+		let signed_tx = signer.sign_transaction(unsigned_tx.into());
+		let bytes = signed_tx.signed_payload();
 
 		let hash = client
 			.send_raw_transaction(bytes.into())
 			.await
 			.with_context(|| "transaction failed")?;
 
-		Ok(hash)
-	}
-
-	/// Send the transaction and wait for the receipt.
-	pub async fn send_and_wait_for_receipt(
-		self,
-		client: &(impl EthRpcClient + Send + Sync),
-	) -> anyhow::Result<ReceiptInfo> {
-		let hash = self.send(client).await?;
-		wait_for_successful_receipt(client, hash).await
+		Ok(SubmittedTransaction {
+			tx: GenericTransaction::from_signed(signed_tx, Some(from)),
+			hash,
+			client,
+		})
 	}
 }
diff --git a/substrate/frame/revive/rpc/src/lib.rs b/substrate/frame/revive/rpc/src/lib.rs
index f2567db8a330d..3599083dcd431 100644
--- a/substrate/frame/revive/rpc/src/lib.rs
+++ b/substrate/frame/revive/rpc/src/lib.rs
@@ -41,6 +41,9 @@ pub use block_info_provider::*;
 mod receipt_provider;
 pub use receipt_provider::*;
 
+mod receipt_extractor;
+pub use receipt_extractor::*;
+
 mod rpc_health;
 pub use rpc_health::*;
 
@@ -299,8 +302,10 @@ impl EthRpcServer for EthRpcServerImpl {
 		block_hash: H256,
 		transaction_index: U256,
 	) -> RpcResult<Option<TransactionInfo>> {
-		let Some(receipt) =
-			self.client.receipt_by_hash_and_index(&block_hash, &transaction_index).await
+		let Some(receipt) = self
+			.client
+			.receipt_by_hash_and_index(&block_hash, transaction_index.as_usize())
+			.await
 		else {
 			return Ok(None);
 		};
diff --git a/substrate/frame/revive/rpc/src/receipt_extractor.rs b/substrate/frame/revive/rpc/src/receipt_extractor.rs
new file mode 100644
index 0000000000000..e53f98639671c
--- /dev/null
+++ b/substrate/frame/revive/rpc/src/receipt_extractor.rs
@@ -0,0 +1,178 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::{
+	client::SubstrateBlock,
+	subxt_client::{
+		revive::{calls::types::EthTransact, events::ContractEmitted},
+		system::events::ExtrinsicSuccess,
+		transaction_payment::events::TransactionFeePaid,
+		SrcChainConfig,
+	},
+	ClientError, LOG_TARGET,
+};
+use futures::{stream, StreamExt};
+use pallet_revive::{
+	create1,
+	evm::{GenericTransaction, Log, ReceiptInfo, TransactionSigned, H256, U256},
+};
+use sp_core::keccak_256;
+
+/// Utility to extract receipts from extrinsics.
+#[derive(Clone, Debug)]
+pub struct ReceiptExtractor {
+	/// The native to eth decimal ratio, used to calculated gas from native fees.
+	native_to_eth_ratio: u32,
+}
+
+impl ReceiptExtractor {
+	/// Create a new `ReceiptExtractor` with the given native to eth ratio.
+	pub fn new(native_to_eth_ratio: u32) -> Self {
+		Self { native_to_eth_ratio }
+	}
+
+	/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] and  from an extrinsic.
+	pub async fn extract_from_extrinsic(
+		&self,
+		block: &SubstrateBlock,
+		ext: subxt::blocks::ExtrinsicDetails<SrcChainConfig, subxt::OnlineClient<SrcChainConfig>>,
+		call: EthTransact,
+	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
+		let transaction_index = ext.index();
+		let block_number = U256::from(block.number());
+		let block_hash = block.hash();
+		let events = ext.events().await?;
+
+		let success = events.has::<ExtrinsicSuccess>().inspect_err(|err| {
+		log::debug!(target: LOG_TARGET, "Failed to lookup for ExtrinsicSuccess event in block {block_number}: {err:?}")
+	})?;
+		let tx_fees = events
+		.find_first::<TransactionFeePaid>()?
+		.ok_or(ClientError::TxFeeNotFound)
+		.inspect_err(
+			|err| log::debug!(target: LOG_TARGET, "TransactionFeePaid not found in events for block {block_number}\n{err:?}")
+		)?;
+		let transaction_hash = H256(keccak_256(&call.payload));
+
+		let signed_tx =
+			TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
+		let from = signed_tx.recover_eth_address().map_err(|_| {
+			log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
+			ClientError::RecoverEthAddressFailed
+		})?;
+
+		let tx_info = GenericTransaction::from_signed(signed_tx.clone(), Some(from));
+		let gas_price = tx_info.gas_price.unwrap_or_default();
+		let gas_used = U256::from(tx_fees.tip.saturating_add(tx_fees.actual_fee))
+			.saturating_mul(self.native_to_eth_ratio.into())
+			.checked_div(gas_price)
+			.unwrap_or_default();
+
+		// get logs from ContractEmitted event
+		let logs = events
+			.iter()
+			.filter_map(|event_details| {
+				let event_details = event_details.ok()?;
+				let event = event_details.as_event::<ContractEmitted>().ok()??;
+
+				Some(Log {
+					address: event.contract,
+					topics: event.topics,
+					data: Some(event.data.into()),
+					block_number: Some(block_number),
+					transaction_hash,
+					transaction_index: Some(transaction_index.into()),
+					block_hash: Some(block_hash),
+					log_index: Some(event_details.index().into()),
+					..Default::default()
+				})
+			})
+			.collect();
+
+		let contract_address = if tx_info.to.is_none() {
+			Some(create1(
+				&from,
+				tx_info
+					.nonce
+					.unwrap_or_default()
+					.try_into()
+					.map_err(|_| ClientError::ConversionFailed)?,
+			))
+		} else {
+			None
+		};
+
+		log::debug!(target: LOG_TARGET, "Adding receipt for tx hash: {transaction_hash:?} - block: {block_number:?}");
+		let receipt = ReceiptInfo::new(
+			block_hash,
+			block_number,
+			contract_address,
+			from,
+			logs,
+			tx_info.to,
+			gas_price,
+			gas_used,
+			success,
+			transaction_hash,
+			transaction_index.into(),
+			tx_info.r#type.unwrap_or_default(),
+		);
+		Ok((signed_tx, receipt))
+	}
+
+	///  Extract receipts from block.
+	pub async fn extract_from_block(
+		&self,
+		block: &SubstrateBlock,
+	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
+		// Filter extrinsics from pallet_revive
+		let extrinsics = block.extrinsics().await.inspect_err(|err| {
+			log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.number());
+		})?;
+
+		let extrinsics = extrinsics.iter().flat_map(|ext| {
+			let call = ext.as_extrinsic::<EthTransact>().ok()??;
+			Some((ext, call))
+		});
+
+		stream::iter(extrinsics)
+			.map(|(ext, call)| async move { self.extract_from_extrinsic(block, ext, call).await })
+			.buffer_unordered(10)
+			.collect::<Vec<Result<_, _>>>()
+			.await
+			.into_iter()
+			.collect::<Result<Vec<_>, _>>()
+	}
+
+	///  Extract receipt from transaction
+	pub async fn extract_from_transaction(
+		&self,
+		block: &SubstrateBlock,
+		transaction_index: usize,
+	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
+		let extrinsics = block.extrinsics().await?;
+		let ext = extrinsics
+			.iter()
+			.nth(transaction_index)
+			.ok_or(ClientError::EthExtrinsicNotFound)?;
+
+		let call = ext
+			.as_extrinsic::<EthTransact>()?
+			.ok_or_else(|| ClientError::EthExtrinsicNotFound)?;
+		self.extract_from_extrinsic(block, ext, call).await
+	}
+}
diff --git a/substrate/frame/revive/rpc/src/receipt_provider.rs b/substrate/frame/revive/rpc/src/receipt_provider.rs
index 5c102b3d3d41a..7c5e33cf01e39 100644
--- a/substrate/frame/revive/rpc/src/receipt_provider.rs
+++ b/substrate/frame/revive/rpc/src/receipt_provider.rs
@@ -15,23 +15,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::{
-	client::SubstrateBlock,
-	subxt_client::{
-		revive::{calls::types::EthTransact, events::ContractEmitted},
-		system::events::ExtrinsicSuccess,
-		transaction_payment::events::TransactionFeePaid,
-		SrcChainConfig,
-	},
-	ClientError, LOG_TARGET,
-};
-use futures::{stream, StreamExt};
 use jsonrpsee::core::async_trait;
-use pallet_revive::{
-	create1,
-	evm::{GenericTransaction, Log, ReceiptInfo, TransactionSigned, H256, U256},
-};
-use sp_core::keccak_256;
+use pallet_revive::evm::{ReceiptInfo, TransactionSigned, H256};
 use tokio::join;
 
 mod cache;
@@ -53,7 +38,7 @@ pub trait ReceiptProvider: Send + Sync {
 	async fn receipt_by_block_hash_and_index(
 		&self,
 		block_hash: &H256,
-		transaction_index: &U256,
+		transaction_index: usize,
 	) -> Option<ReceiptInfo>;
 
 	/// Get the number of receipts per block.
@@ -79,7 +64,7 @@ impl<Main: ReceiptProvider, Fallback: ReceiptProvider> ReceiptProvider for (Main
 	async fn receipt_by_block_hash_and_index(
 		&self,
 		block_hash: &H256,
-		transaction_index: &U256,
+		transaction_index: usize,
 	) -> Option<ReceiptInfo> {
 		if let Some(receipt) =
 			self.0.receipt_by_block_hash_and_index(block_hash, transaction_index).await
@@ -111,130 +96,3 @@ impl<Main: ReceiptProvider, Fallback: ReceiptProvider> ReceiptProvider for (Main
 		self.1.signed_tx_by_hash(hash).await
 	}
 }
-
-/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] and  from an extrinsic.
-pub async fn extract_receipt_from_extrinsic(
-	block: &SubstrateBlock,
-	ext: subxt::blocks::ExtrinsicDetails<SrcChainConfig, subxt::OnlineClient<SrcChainConfig>>,
-	call: EthTransact,
-) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
-	let transaction_index = ext.index();
-	let block_number = U256::from(block.number());
-	let block_hash = block.hash();
-	let events = ext.events().await?;
-
-	let success = events.has::<ExtrinsicSuccess>().inspect_err(|err| {
-		log::debug!(target: LOG_TARGET, "Failed to lookup for ExtrinsicSuccess event in block {block_number}: {err:?}")
-	})?;
-	let tx_fees = events
-		.find_first::<TransactionFeePaid>()?
-		.ok_or(ClientError::TxFeeNotFound)
-		.inspect_err(
-			|err| log::debug!(target: LOG_TARGET, "TransactionFeePaid not found in events for block {block_number}\n{err:?}")
-		)?;
-	let transaction_hash = H256(keccak_256(&call.payload));
-
-	let signed_tx =
-		TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
-	let from = signed_tx.recover_eth_address().map_err(|_| {
-		log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
-		ClientError::RecoverEthAddressFailed
-	})?;
-
-	let tx_info = GenericTransaction::from_signed(signed_tx.clone(), Some(from));
-	let gas_price = tx_info.gas_price.unwrap_or_default();
-	let gas_used = (tx_fees.tip.saturating_add(tx_fees.actual_fee))
-		.checked_div(gas_price.as_u128())
-		.unwrap_or_default();
-
-	// get logs from ContractEmitted event
-	let logs = events
-		.iter()
-		.filter_map(|event_details| {
-			let event_details = event_details.ok()?;
-			let event = event_details.as_event::<ContractEmitted>().ok()??;
-
-			Some(Log {
-				address: event.contract,
-				topics: event.topics,
-				data: Some(event.data.into()),
-				block_number: Some(block_number),
-				transaction_hash,
-				transaction_index: Some(transaction_index.into()),
-				block_hash: Some(block_hash),
-				log_index: Some(event_details.index().into()),
-				..Default::default()
-			})
-		})
-		.collect();
-
-	let contract_address = if tx_info.to.is_none() {
-		Some(create1(
-			&from,
-			tx_info
-				.nonce
-				.unwrap_or_default()
-				.try_into()
-				.map_err(|_| ClientError::ConversionFailed)?,
-		))
-	} else {
-		None
-	};
-
-	log::debug!(target: LOG_TARGET, "Adding receipt for tx hash: {transaction_hash:?} - block: {block_number:?}");
-	let receipt = ReceiptInfo::new(
-		block_hash,
-		block_number,
-		contract_address,
-		from,
-		logs,
-		tx_info.to,
-		gas_price,
-		gas_used.into(),
-		success,
-		transaction_hash,
-		transaction_index.into(),
-		tx_info.r#type.unwrap_or_default(),
-	);
-	Ok((signed_tx, receipt))
-}
-
-///  Extract receipts from block.
-pub async fn extract_receipts_from_block(
-	block: &SubstrateBlock,
-) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
-	// Filter extrinsics from pallet_revive
-	let extrinsics = block.extrinsics().await.inspect_err(|err| {
-		log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.number());
-	})?;
-
-	let extrinsics = extrinsics.iter().flat_map(|ext| {
-		let call = ext.as_extrinsic::<EthTransact>().ok()??;
-		Some((ext, call))
-	});
-
-	stream::iter(extrinsics)
-		.map(|(ext, call)| async move { extract_receipt_from_extrinsic(block, ext, call).await })
-		.buffer_unordered(10)
-		.collect::<Vec<Result<_, _>>>()
-		.await
-		.into_iter()
-		.collect::<Result<Vec<_>, _>>()
-}
-
-///  Extract receipt from transaction
-pub async fn extract_receipts_from_transaction(
-	block: &SubstrateBlock,
-	transaction_index: usize,
-) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
-	let extrinsics = block.extrinsics().await?;
-	let ext = extrinsics
-		.iter()
-		.nth(transaction_index)
-		.ok_or(ClientError::EthExtrinsicNotFound)?;
-
-	let call = ext
-		.as_extrinsic::<EthTransact>()?
-		.ok_or_else(|| ClientError::EthExtrinsicNotFound)?;
-	extract_receipt_from_extrinsic(block, ext, call).await
-}
diff --git a/substrate/frame/revive/rpc/src/receipt_provider/cache.rs b/substrate/frame/revive/rpc/src/receipt_provider/cache.rs
index 39124929ec07d..a4741d18a3b34 100644
--- a/substrate/frame/revive/rpc/src/receipt_provider/cache.rs
+++ b/substrate/frame/revive/rpc/src/receipt_provider/cache.rs
@@ -16,7 +16,7 @@
 // limitations under the License.
 use super::ReceiptProvider;
 use jsonrpsee::core::async_trait;
-use pallet_revive::evm::{ReceiptInfo, TransactionSigned, H256, U256};
+use pallet_revive::evm::{ReceiptInfo, TransactionSigned, H256};
 use std::{collections::HashMap, sync::Arc};
 use tokio::sync::RwLock;
 
@@ -48,13 +48,13 @@ impl ReceiptProvider for CacheReceiptProvider {
 	async fn receipt_by_block_hash_and_index(
 		&self,
 		block_hash: &H256,
-		transaction_index: &U256,
+		transaction_index: usize,
 	) -> Option<ReceiptInfo> {
 		let cache = self.cache().await;
 		let receipt_hash = cache
 			.transaction_hashes_by_block_and_index
 			.get(block_hash)?
-			.get(transaction_index)?;
+			.get(&transaction_index)?;
 		let receipt = cache.receipts_by_hash.get(receipt_hash)?;
 		Some(receipt.clone())
 	}
@@ -84,7 +84,7 @@ struct ReceiptCache {
 	signed_tx_by_hash: HashMap<H256, TransactionSigned>,
 
 	/// A map of receipt hashes by block hash.
-	transaction_hashes_by_block_and_index: HashMap<H256, HashMap<U256, H256>>,
+	transaction_hashes_by_block_and_index: HashMap<H256, HashMap<usize, H256>>,
 }
 
 impl ReceiptCache {
@@ -93,7 +93,9 @@ impl ReceiptCache {
 		if !receipts.is_empty() {
 			let values = receipts
 				.iter()
-				.map(|(_, receipt)| (receipt.transaction_index, receipt.transaction_hash))
+				.map(|(_, receipt)| {
+					(receipt.transaction_index.as_usize(), receipt.transaction_hash)
+				})
 				.collect::<HashMap<_, _>>();
 
 			self.transaction_hashes_by_block_and_index.insert(*block_hash, values);
diff --git a/substrate/frame/revive/rpc/src/receipt_provider/db.rs b/substrate/frame/revive/rpc/src/receipt_provider/db.rs
index 63917d6193ea7..15f4119b4aefd 100644
--- a/substrate/frame/revive/rpc/src/receipt_provider/db.rs
+++ b/substrate/frame/revive/rpc/src/receipt_provider/db.rs
@@ -16,10 +16,10 @@
 // limitations under the License.
 
 use super::*;
-use crate::BlockInfoProvider;
+use crate::{BlockInfoProvider, ReceiptExtractor};
 use jsonrpsee::core::async_trait;
 use pallet_revive::evm::{ReceiptInfo, TransactionSigned};
-use sp_core::{H256, U256};
+use sp_core::H256;
 use sqlx::{query, SqlitePool};
 use std::sync::Arc;
 
@@ -30,6 +30,8 @@ pub struct DBReceiptProvider {
 	pool: SqlitePool,
 	/// The block provider used to fetch blocks, and reconstruct receipts.
 	block_provider: Arc<dyn BlockInfoProvider>,
+	/// A means to extract receipts from extrinsics.
+	receipt_extractor: ReceiptExtractor,
 	/// weather or not we should write to the DB.
 	read_only: bool,
 }
@@ -40,9 +42,10 @@ impl DBReceiptProvider {
 		database_url: &str,
 		read_only: bool,
 		block_provider: Arc<dyn BlockInfoProvider>,
+		receipt_extractor: ReceiptExtractor,
 	) -> Result<Self, sqlx::Error> {
 		let pool = SqlitePool::connect(database_url).await?;
-		Ok(Self { pool, block_provider, read_only })
+		Ok(Self { pool, block_provider, read_only, receipt_extractor })
 	}
 
 	async fn fetch_row(&self, transaction_hash: &H256) -> Option<(H256, usize)> {
@@ -125,12 +128,14 @@ impl ReceiptProvider for DBReceiptProvider {
 	async fn receipt_by_block_hash_and_index(
 		&self,
 		block_hash: &H256,
-		transaction_index: &U256,
+		transaction_index: usize,
 	) -> Option<ReceiptInfo> {
 		let block = self.block_provider.block_by_hash(block_hash).await.ok()??;
-		let transaction_index: usize = transaction_index.as_usize(); // TODO: check for overflow
-		let (_, receipt) =
-			extract_receipts_from_transaction(&block, transaction_index).await.ok()?;
+		let (_, receipt) = self
+			.receipt_extractor
+			.extract_from_transaction(&block, transaction_index)
+			.await
+			.ok()?;
 		Some(receipt)
 	}
 
@@ -138,8 +143,11 @@ impl ReceiptProvider for DBReceiptProvider {
 		let (block_hash, transaction_index) = self.fetch_row(transaction_hash).await?;
 
 		let block = self.block_provider.block_by_hash(&block_hash).await.ok()??;
-		let (_, receipt) =
-			extract_receipts_from_transaction(&block, transaction_index).await.ok()?;
+		let (_, receipt) = self
+			.receipt_extractor
+			.extract_from_transaction(&block, transaction_index)
+			.await
+			.ok()?;
 		Some(receipt)
 	}
 
@@ -161,8 +169,11 @@ impl ReceiptProvider for DBReceiptProvider {
 		let transaction_index = result.transaction_index.try_into().ok()?;
 
 		let block = self.block_provider.block_by_hash(&block_hash).await.ok()??;
-		let (signed_tx, _) =
-			extract_receipts_from_transaction(&block, transaction_index).await.ok()?;
+		let (signed_tx, _) = self
+			.receipt_extractor
+			.extract_from_transaction(&block, transaction_index)
+			.await
+			.ok()?;
 		Some(signed_tx)
 	}
 }
@@ -179,6 +190,7 @@ mod tests {
 		DBReceiptProvider {
 			pool,
 			block_provider: Arc::new(MockBlockInfoProvider {}),
+			receipt_extractor: ReceiptExtractor::new(1_000_000),
 			read_only: false,
 		}
 	}


Test patch:
diff --git a/substrate/frame/revive/rpc/src/eth-rpc-tester.rs b/substrate/frame/revive/rpc/src/eth-rpc-tester.rs
index 0ddad6874dfd5..7cea1a303e383 100644
--- a/substrate/frame/revive/rpc/src/eth-rpc-tester.rs
+++ b/substrate/frame/revive/rpc/src/eth-rpc-tester.rs
@@ -17,10 +17,8 @@
 use clap::Parser;
 use jsonrpsee::http_client::HttpClientBuilder;
 use pallet_revive::evm::{Account, BlockTag, ReceiptInfo};
-use pallet_revive_eth_rpc::{
-	example::{wait_for_receipt, TransactionBuilder},
-	EthRpcClient,
-};
+use pallet_revive_eth_rpc::{example::TransactionBuilder, EthRpcClient};
+use std::sync::Arc;
 use tokio::{
 	io::{AsyncBufReadExt, BufReader},
 	process::{Child, ChildStderr, Command},
@@ -119,7 +117,7 @@ async fn test_eth_rpc(stderr: ChildStderr) -> anyhow::Result<()> {
 
 	println!("Account:");
 	println!("- address: {:?}", account.address());
-	let client = HttpClientBuilder::default().build("http://localhost:8545")?;
+	let client = Arc::new(HttpClientBuilder::default().build("http://localhost:8545")?);
 
 	let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?;
 	let balance = client.get_balance(account.address(), BlockTag::Latest.into()).await?;
@@ -127,29 +125,29 @@ async fn test_eth_rpc(stderr: ChildStderr) -> anyhow::Result<()> {
 	println!("-  balance: {balance:?}");
 
 	println!("\n\n=== Deploying dummy contract ===\n\n");
-	let hash = TransactionBuilder::default().input(input).send(&client).await?;
+	let tx = TransactionBuilder::new(&client).input(input).send().await?;
 
-	println!("Hash: {hash:?}");
+	println!("Hash: {:?}", tx.hash());
 	println!("Waiting for receipt...");
 	let ReceiptInfo { block_number, gas_used, contract_address, .. } =
-		wait_for_receipt(&client, hash).await?;
+		tx.wait_for_receipt().await?;
 
 	let contract_address = contract_address.unwrap();
 	println!("\nReceipt:");
-	println!("Block explorer: https://westend-asset-hub-eth-explorer.parity.io/{hash:?}");
+	println!("Block explorer: https://westend-asset-hub-eth-explorer.parity.io/{:?}", tx.hash());
 	println!("- Block number: {block_number}");
 	println!("- Gas used: {gas_used}");
 	println!("- Address: {contract_address:?}");
 
 	println!("\n\n=== Calling dummy contract ===\n\n");
-	let hash = TransactionBuilder::default().to(contract_address).send(&client).await?;
+	let tx = TransactionBuilder::new(&client).to(contract_address).send().await?;
 
-	println!("Hash: {hash:?}");
+	println!("Hash: {:?}", tx.hash());
 	println!("Waiting for receipt...");
 
-	let ReceiptInfo { block_number, gas_used, to, .. } = wait_for_receipt(&client, hash).await?;
+	let ReceiptInfo { block_number, gas_used, to, .. } = tx.wait_for_receipt().await?;
 	println!("\nReceipt:");
-	println!("Block explorer: https://westend-asset-hub-eth-explorer.parity.io/{hash:?}");
+	println!("Block explorer: https://westend-asset-hub-eth-explorer.parity.io/{:?}", tx.hash());
 	println!("- Block number: {block_number}");
 	println!("- Gas used: {gas_used}");
 	println!("- To: {to:?}");
diff --git a/substrate/frame/revive/rpc/src/tests.rs b/substrate/frame/revive/rpc/src/tests.rs
index e64e16d45b2ae..e1ac274d32ea8 100644
--- a/substrate/frame/revive/rpc/src/tests.rs
+++ b/substrate/frame/revive/rpc/src/tests.rs
@@ -18,7 +18,7 @@
 
 use crate::{
 	cli::{self, CliCommand},
-	example::{wait_for_successful_receipt, TransactionBuilder},
+	example::TransactionBuilder,
 	EthRpcClient,
 };
 use clap::Parser;
@@ -29,7 +29,7 @@ use pallet_revive::{
 	evm::{Account, BlockTag, U256},
 };
 use static_init::dynamic;
-use std::thread;
+use std::{sync::Arc, thread};
 use substrate_cli_test_utils::*;
 
 /// Create a websocket client with a 120s timeout.
@@ -117,19 +117,15 @@ macro_rules! unwrap_call_err(
 #[tokio::test]
 async fn transfer() -> anyhow::Result<()> {
 	let _lock = SHARED_RESOURCES.write();
-	let client = SharedResources::client().await;
+	let client = Arc::new(SharedResources::client().await);
 
 	let ethan = Account::from(subxt_signer::eth::dev::ethan());
 	let initial_balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?;
 
 	let value = 1_000_000_000_000_000_000_000u128.into();
-	let hash = TransactionBuilder::default()
-		.value(value)
-		.to(ethan.address())
-		.send(&client)
-		.await?;
+	let tx = TransactionBuilder::new(&client).value(value).to(ethan.address()).send().await?;
 
-	let receipt = wait_for_successful_receipt(&client, hash).await?;
+	let receipt = tx.wait_for_receipt().await?;
 	assert_eq!(
 		Some(ethan.address()),
 		receipt.to,
@@ -145,7 +141,7 @@ async fn transfer() -> anyhow::Result<()> {
 #[tokio::test]
 async fn deploy_and_call() -> anyhow::Result<()> {
 	let _lock = SHARED_RESOURCES.write();
-	let client = SharedResources::client().await;
+	let client = std::sync::Arc::new(SharedResources::client().await);
 	let account = Account::default();
 
 	// Balance transfer
@@ -153,13 +149,9 @@ async fn deploy_and_call() -> anyhow::Result<()> {
 	let initial_balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?;
 
 	let value = 1_000_000_000_000_000_000_000u128.into();
-	let hash = TransactionBuilder::default()
-		.value(value)
-		.to(ethan.address())
-		.send(&client)
-		.await?;
+	let tx = TransactionBuilder::new(&client).value(value).to(ethan.address()).send().await?;
 
-	let receipt = wait_for_successful_receipt(&client, hash).await?;
+	let receipt = tx.wait_for_receipt().await?;
 	assert_eq!(
 		Some(ethan.address()),
 		receipt.to,
@@ -175,8 +167,8 @@ async fn deploy_and_call() -> anyhow::Result<()> {
 	let (bytes, _) = pallet_revive_fixtures::compile_module("dummy")?;
 	let input = bytes.into_iter().chain(data.clone()).collect::<Vec<u8>>();
 	let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?;
-	let hash = TransactionBuilder::default().value(value).input(input).send(&client).await?;
-	let receipt = wait_for_successful_receipt(&client, hash).await?;
+	let tx = TransactionBuilder::new(&client).value(value).input(input).send().await?;
+	let receipt = tx.wait_for_receipt().await?;
 	let contract_address = create1(&account.address(), nonce.try_into().unwrap());
 	assert_eq!(
 		Some(contract_address),
@@ -188,12 +180,12 @@ async fn deploy_and_call() -> anyhow::Result<()> {
 	assert_eq!(value, balance, "Contract balance should be the same as the value sent.");
 
 	// Call contract
-	let hash = TransactionBuilder::default()
+	let tx = TransactionBuilder::new(&client)
 		.value(value)
 		.to(contract_address)
-		.send(&client)
+		.send()
 		.await?;
-	let receipt = wait_for_successful_receipt(&client, hash).await?;
+	let receipt = tx.wait_for_receipt().await?;
 
 	assert_eq!(
 		Some(contract_address),
@@ -206,13 +198,13 @@ async fn deploy_and_call() -> anyhow::Result<()> {
 
 	// Balance transfer to contract
 	let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?;
-	let hash = TransactionBuilder::default()
+	let tx = TransactionBuilder::new(&client)
 		.value(value)
 		.to(contract_address)
-		.send(&client)
+		.send()
 		.await?;
 
-	wait_for_successful_receipt(&client, hash).await?;
+	tx.wait_for_receipt().await?;
 	let increase = client.get_balance(contract_address, BlockTag::Latest.into()).await? - balance;
 	assert_eq!(value, increase, "contract's balance should have increased by the value sent.");
 	Ok(())
@@ -221,17 +213,19 @@ async fn deploy_and_call() -> anyhow::Result<()> {
 #[tokio::test]
 async fn revert_call() -> anyhow::Result<()> {
 	let _lock = SHARED_RESOURCES.write();
-	let client = SharedResources::client().await;
+	let client = Arc::new(SharedResources::client().await);
 	let (bytecode, contract) = get_contract("Errors")?;
-	let receipt = TransactionBuilder::default()
+	let receipt = TransactionBuilder::new(&client)
 		.input(bytecode)
-		.send_and_wait_for_receipt(&client)
+		.send()
+		.await?
+		.wait_for_receipt()
 		.await?;
 
-	let err = TransactionBuilder::default()
+	let err = TransactionBuilder::new(&client)
 		.to(receipt.contract_address.unwrap())
 		.input(contract.function("triggerRequireError")?.encode_input(&[])?.to_vec())
-		.send(&client)
+		.send()
 		.await
 		.unwrap_err();
 
@@ -244,17 +238,21 @@ async fn revert_call() -> anyhow::Result<()> {
 #[tokio::test]
 async fn event_logs() -> anyhow::Result<()> {
 	let _lock = SHARED_RESOURCES.write();
-	let client = SharedResources::client().await;
+	let client = Arc::new(SharedResources::client().await);
 	let (bytecode, contract) = get_contract("EventExample")?;
-	let receipt = TransactionBuilder::default()
+	let receipt = TransactionBuilder::new(&client)
 		.input(bytecode)
-		.send_and_wait_for_receipt(&client)
+		.send()
+		.await?
+		.wait_for_receipt()
 		.await?;
 
-	let receipt = TransactionBuilder::default()
+	let receipt = TransactionBuilder::new(&client)
 		.to(receipt.contract_address.unwrap())
 		.input(contract.function("triggerEvent")?.encode_input(&[])?.to_vec())
-		.send_and_wait_for_receipt(&client)
+		.send()
+		.await?
+		.wait_for_receipt()
 		.await?;
 	assert_eq!(receipt.logs.len(), 1, "There should be one log.");
 	Ok(())
@@ -263,14 +261,14 @@ async fn event_logs() -> anyhow::Result<()> {
 #[tokio::test]
 async fn invalid_transaction() -> anyhow::Result<()> {
 	let _lock = SHARED_RESOURCES.write();
-	let client = SharedResources::client().await;
+	let client = Arc::new(SharedResources::client().await);
 	let ethan = Account::from(subxt_signer::eth::dev::ethan());
 
-	let err = TransactionBuilder::default()
+	let err = TransactionBuilder::new(&client)
 		.value(U256::from(1_000_000_000_000u128))
 		.to(ethan.address())
 		.mutate(|tx| tx.chain_id = Some(42u32.into()))
-		.send(&client)
+		.send()
 		.await
 		.unwrap_err();
 
@@ -283,28 +281,32 @@ async fn invalid_transaction() -> anyhow::Result<()> {
 #[tokio::test]
 async fn native_evm_ratio_works() -> anyhow::Result<()> {
 	let _lock = SHARED_RESOURCES.write();
-	let client = SharedResources::client().await;
+	let client = Arc::new(SharedResources::client().await);
 	let (bytecode, contract) = get_contract("PiggyBank")?;
-	let contract_address = TransactionBuilder::default()
+	let contract_address = TransactionBuilder::new(&client)
 		.input(bytecode)
-		.send_and_wait_for_receipt(&client)
+		.send()
+		.await?
+		.wait_for_receipt()
 		.await?
 		.contract_address
 		.unwrap();
 
 	let value = 10_000_000_000_000_000_000u128; // 10 eth
-	TransactionBuilder::default()
+	TransactionBuilder::new(&client)
 		.to(contract_address)
 		.input(contract.function("deposit")?.encode_input(&[])?.to_vec())
 		.value(value.into())
-		.send_and_wait_for_receipt(&client)
+		.send()
+		.await?
+		.wait_for_receipt()
 		.await?;
 
 	let contract_value = client.get_balance(contract_address, BlockTag::Latest.into()).await?;
 	assert_eq!(contract_value, value.into());
 
 	let withdraw_value = 1_000_000_000_000_000_000u128; // 1 eth
-	TransactionBuilder::default()
+	TransactionBuilder::new(&client)
 		.to(contract_address)
 		.input(
 			contract
@@ -312,7 +314,9 @@ async fn native_evm_ratio_works() -> anyhow::Result<()> {
 				.encode_input(&[Token::Uint(withdraw_value.into())])?
 				.to_vec(),
 		)
-		.send_and_wait_for_receipt(&client)
+		.send()
+		.await?
+		.wait_for_receipt()
 		.await?;
 
 	let contract_value = client.get_balance(contract_address, BlockTag::Latest.into()).await?;