referrerpolicy=no-referrer-when-downgrade

pallet_revive_eth_rpc/
receipt_extractor.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.
17use crate::{
18	client::{SubstrateBlock, SubstrateBlockNumber},
19	subxt_client::{
20		self,
21		revive::{calls::types::EthTransact, events::ContractEmitted},
22		system::events::ExtrinsicSuccess,
23		transaction_payment::events::TransactionFeePaid,
24		SrcChainConfig,
25	},
26	ClientError, LOG_TARGET,
27};
28use futures::{stream, StreamExt};
29use pallet_revive::{
30	create1,
31	evm::{GenericTransaction, Log, ReceiptInfo, TransactionSigned, H256, U256},
32};
33use sp_core::keccak_256;
34use std::{future::Future, pin::Pin, sync::Arc};
35use subxt::OnlineClient;
36
37type FetchGasPriceFn = Arc<
38	dyn Fn(H256) -> Pin<Box<dyn Future<Output = Result<U256, ClientError>> + Send>> + Send + Sync,
39>;
40/// Utility to extract receipts from extrinsics.
41#[derive(Clone)]
42pub struct ReceiptExtractor {
43	/// Fetch the gas price from the chain.
44	fetch_gas_price: FetchGasPriceFn,
45
46	/// The native to eth decimal ratio, used to calculated gas from native fees.
47	native_to_eth_ratio: u32,
48
49	/// Earliest block number to consider when searching for transaction receipts.
50	earliest_receipt_block: Option<SubstrateBlockNumber>,
51}
52
53/// Fetch the native_to_eth_ratio
54async fn native_to_eth_ratio(api: &OnlineClient<SrcChainConfig>) -> Result<u32, ClientError> {
55	let query = subxt_client::constants().revive().native_to_eth_ratio();
56	api.constants().at(&query).map_err(|err| err.into())
57}
58
59impl ReceiptExtractor {
60	/// Check if the block is before the earliest block.
61	pub fn is_before_earliest_block(&self, block_number: SubstrateBlockNumber) -> bool {
62		block_number < self.earliest_receipt_block.unwrap_or_default()
63	}
64
65	/// Create a new `ReceiptExtractor` with the given native to eth ratio.
66	pub async fn new(
67		api: OnlineClient<SrcChainConfig>,
68		earliest_receipt_block: Option<SubstrateBlockNumber>,
69	) -> Result<Self, ClientError> {
70		let native_to_eth_ratio = native_to_eth_ratio(&api).await?;
71
72		let fetch_gas_price = Arc::new(move |block_hash| {
73			let api_clone = api.clone();
74			let fut = async move {
75				let runtime_api = api_clone.runtime_api().at(block_hash);
76				let payload = subxt_client::apis().revive_api().gas_price();
77				let base_gas_price = runtime_api.call(payload).await?;
78				Ok(*base_gas_price)
79			};
80			Box::pin(fut) as Pin<Box<_>>
81		});
82
83		Ok(Self { native_to_eth_ratio, fetch_gas_price, earliest_receipt_block })
84	}
85
86	#[cfg(test)]
87	pub fn new_mock() -> Self {
88		let fetch_gas_price =
89			Arc::new(|_| Box::pin(std::future::ready(Ok(U256::from(1000)))) as Pin<Box<_>>);
90
91		Self { native_to_eth_ratio: 1_000_000, fetch_gas_price, earliest_receipt_block: None }
92	}
93
94	/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] from an extrinsic.
95	async fn extract_from_extrinsic(
96		&self,
97		block: &SubstrateBlock,
98		ext: subxt::blocks::ExtrinsicDetails<SrcChainConfig, subxt::OnlineClient<SrcChainConfig>>,
99		call: EthTransact,
100	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
101		let transaction_index = ext.index();
102		let block_number = U256::from(block.number());
103		let block_hash = block.hash();
104		let events = ext.events().await?;
105
106		let success = events.has::<ExtrinsicSuccess>().inspect_err(|err| {
107		log::debug!(target: LOG_TARGET, "Failed to lookup for ExtrinsicSuccess event in block {block_number}: {err:?}")
108	})?;
109		let tx_fees = events
110		.find_first::<TransactionFeePaid>()?
111		.ok_or(ClientError::TxFeeNotFound)
112		.inspect_err(
113			|err| log::debug!(target: LOG_TARGET, "TransactionFeePaid not found in events for block {block_number}\n{err:?}")
114		)?;
115		let transaction_hash = H256(keccak_256(&call.payload));
116
117		let signed_tx =
118			TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
119		let from = signed_tx.recover_eth_address().map_err(|_| {
120			log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
121			ClientError::RecoverEthAddressFailed
122		})?;
123
124		let base_gas_price = (self.fetch_gas_price)(block_hash).await?;
125		let tx_info =
126			GenericTransaction::from_signed(signed_tx.clone(), base_gas_price, Some(from));
127		let gas_price = tx_info.gas_price.unwrap_or_default();
128		let gas_used = U256::from(tx_fees.tip.saturating_add(tx_fees.actual_fee))
129			.saturating_mul(self.native_to_eth_ratio.into())
130			.checked_div(gas_price)
131			.unwrap_or_default();
132
133		// get logs from ContractEmitted event
134		let logs = events
135			.iter()
136			.filter_map(|event_details| {
137				let event_details = event_details.ok()?;
138				let event = event_details.as_event::<ContractEmitted>().ok()??;
139
140				Some(Log {
141					address: event.contract,
142					topics: event.topics,
143					data: Some(event.data.into()),
144					block_number,
145					transaction_hash,
146					transaction_index: transaction_index.into(),
147					block_hash,
148					log_index: event_details.index().into(),
149					..Default::default()
150				})
151			})
152			.collect();
153
154		let contract_address = if tx_info.to.is_none() {
155			Some(create1(
156				&from,
157				tx_info
158					.nonce
159					.unwrap_or_default()
160					.try_into()
161					.map_err(|_| ClientError::ConversionFailed)?,
162			))
163		} else {
164			None
165		};
166
167		let receipt = ReceiptInfo::new(
168			block_hash,
169			block_number,
170			contract_address,
171			from,
172			logs,
173			tx_info.to,
174			gas_price,
175			gas_used,
176			success,
177			transaction_hash,
178			transaction_index.into(),
179			tx_info.r#type.unwrap_or_default(),
180		);
181		Ok((signed_tx, receipt))
182	}
183
184	/// Extract receipts from block.
185	pub async fn extract_from_block(
186		&self,
187		block: &SubstrateBlock,
188	) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
189		if self.is_before_earliest_block(block.number()) {
190			return Ok(vec![]);
191		}
192
193		// Filter extrinsics from pallet_revive
194		let extrinsics = block.extrinsics().await.inspect_err(|err| {
195			log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.number());
196		})?;
197
198		let extrinsics = extrinsics.iter().flat_map(|ext| {
199			let call = ext.as_extrinsic::<EthTransact>().ok()??;
200			Some((ext, call))
201		});
202
203		stream::iter(extrinsics)
204			.map(|(ext, call)| async move {
205				self.extract_from_extrinsic(block, ext, call).await.inspect_err(|err| {
206					log::warn!(target: LOG_TARGET, "Error extracting extrinsic: {err:?}");
207				})
208			})
209			.buffer_unordered(10)
210			.collect::<Vec<Result<_, _>>>()
211			.await
212			.into_iter()
213			.collect::<Result<Vec<_>, _>>()
214	}
215
216	/// Extract receipt from transaction
217	pub async fn extract_from_transaction(
218		&self,
219		block: &SubstrateBlock,
220		transaction_index: usize,
221	) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
222		let extrinsics = block.extrinsics().await?;
223		let ext = extrinsics
224			.iter()
225			.nth(transaction_index)
226			.ok_or(ClientError::EthExtrinsicNotFound)?;
227
228		let call = ext
229			.as_extrinsic::<EthTransact>()?
230			.ok_or_else(|| ClientError::EthExtrinsicNotFound)?;
231		self.extract_from_extrinsic(block, ext, call).await
232	}
233}