1use crate::{
18 ClientError, H160, LOG_TARGET, Log, ReceiptGasInfoV1, ReceiptInfo, SyntheticTransactionV1,
19 client::{
20 SubstrateBlock, SubstrateBlockNumber,
21 version_aware_runtime_api::VersionAwareRuntimeApiProvider,
22 },
23 subxt_client::{
24 SrcChainConfig,
25 revive::{
26 calls::EthTransact,
27 events::{ContractEmitted, EthExtrinsicRevert},
28 },
29 },
30};
31
32use pallet_revive::{
33 create1,
34 evm::{GenericTransaction, H256, TransactionSigned, U256},
35};
36use sp_crypto_hashing::keccak_256;
37use std::{
38 collections::{BTreeMap, HashMap, HashSet},
39 future::Future,
40 pin::Pin,
41 sync::{
42 Arc,
43 atomic::{AtomicU64, Ordering},
44 },
45};
46use subxt::{
47 client::OfflineClientAtBlockT,
48 events::{DecodeAsEvent, Phase},
49 extrinsics::Extrinsics,
50};
51
52type EventDetails<'a> = subxt::events::Event<'a, SrcChainConfig>;
53
54enum ReviveEvent {
56 Revert,
57 Log(Log),
58}
59
60fn decode_revive_event(
63 event: &EventDetails<'_>,
64 block_number: U256,
65 transaction_hash: H256,
66 transaction_index: usize,
67 block_hash: H256,
68) -> Option<ReviveEvent> {
69 let pallet_name = event.pallet_name();
70 let event_name = event.event_name();
71
72 if EthExtrinsicRevert::is_event(pallet_name, event_name) {
73 return Some(ReviveEvent::Revert);
74 }
75 if ContractEmitted::is_event(pallet_name, event_name) {
76 match event.decode_fields_as::<ContractEmitted>() {
77 Some(Ok(evt)) => {
78 return Some(ReviveEvent::Log(Log {
79 address: evt.contract,
80 topics: evt.topics,
81 data: Some(evt.data.into()),
82 block_number,
83 transaction_hash,
84 transaction_index: transaction_index.into(),
85 block_hash,
86 log_index: event.index().into(),
87 ..Default::default()
88 }));
89 },
90 Some(Err(err)) => log::warn!(
91 target: LOG_TARGET,
92 "Failed to decode ContractEmitted event {} in block {block_number} (tx {transaction_hash:?}): {err:?}, log dropped from receipt",
93 event.index()
94 ),
95 None => {},
97 }
98 }
99 None
100}
101
102fn extract_revive_events(
117 block_events: &subxt::events::Events<SrcChainConfig>,
118 substrate_block_number: SubstrateBlockNumber,
119 eth_block_number: U256,
120 eth_block_hash: H256,
121 eth_tx_hash_for: impl Fn(usize) -> Option<H256>,
122 synthetic_tx_hash: H256,
123 synthetic_tx_index: usize,
124) -> (HashSet<usize>, HashMap<usize, Vec<Log>>, Vec<Log>) {
125 let mut reverted_extrinsics: HashSet<usize> = HashSet::new();
126 let mut logs_by_extrinsic: HashMap<usize, Vec<Log>> = HashMap::new();
127 let mut outside_frame_logs: Vec<Log> = Vec::new();
128
129 for (event_index, event_result) in block_events.iter().enumerate() {
130 let event = match event_result {
131 Ok(e) => e,
132 Err(err) => {
133 log::debug!(
134 target: LOG_TARGET,
135 "Failed to decode event {event_index} in block #{substrate_block_number}: {err:?}"
136 );
137 continue;
138 },
139 };
140
141 let eth_tx = match event.phase() {
146 Phase::ApplyExtrinsic(idx) => {
147 let idx = idx as usize;
148 eth_tx_hash_for(idx).map(|hash| (idx, hash))
149 },
150 Phase::Initialization | Phase::Finalization => None,
151 };
152
153 match eth_tx {
154 Some((extrinsic_index, eth_tx_hash)) => match decode_revive_event(
155 &event,
156 eth_block_number,
157 eth_tx_hash,
158 extrinsic_index,
159 eth_block_hash,
160 ) {
161 Some(ReviveEvent::Revert) => {
162 reverted_extrinsics.insert(extrinsic_index);
163 },
164 Some(ReviveEvent::Log(log)) => {
165 logs_by_extrinsic.entry(extrinsic_index).or_default().push(log);
166 },
167 None => {},
168 },
169 None => {
172 if let Some(ReviveEvent::Log(log)) = decode_revive_event(
173 &event,
174 eth_block_number,
175 synthetic_tx_hash,
176 synthetic_tx_index,
177 eth_block_hash,
178 ) {
179 outside_frame_logs.push(log);
180 }
181 },
182 }
183 }
184
185 (reverted_extrinsics, logs_by_extrinsic, outside_frame_logs)
186}
187
188fn extract_eth_transacts<C: OfflineClientAtBlockT<SrcChainConfig>>(
190 block_extrinsics: &Extrinsics<'_, SrcChainConfig, C>,
191 block_number: SubstrateBlockNumber,
192) -> Result<Vec<(EthTransact, usize)>, ClientError> {
193 let mut extrinsics = Vec::new();
194 for (ext_idx, ext) in block_extrinsics.iter().enumerate() {
195 let ext = match ext {
196 Ok(ext) => ext,
197 Err(err) => {
200 log::debug!(target: LOG_TARGET,
201 "Failed to decode extrinsic {ext_idx} of block #{block_number}: {err:?}");
202 continue;
203 },
204 };
205 match ext.decode_call_data_fields_as::<EthTransact>() {
206 Some(Ok(call)) => extrinsics.push((call, ext_idx)),
207 Some(Err(err)) => {
208 log::error!(target: LOG_TARGET,
209 "Failed to decode the EthTransact call in extrinsic {ext_idx} of block \
210 #{block_number}: {err:?}");
211 return Err(subxt::Error::from(err).into());
212 },
213 None => {},
215 }
216 }
217
218 Ok(extrinsics)
219}
220
221fn synthetic_tx_index<C: OfflineClientAtBlockT<SrcChainConfig>>(
229 block_extrinsics: &Extrinsics<'_, SrcChainConfig, C>,
230) -> usize {
231 block_extrinsics.len()
232}
233
234fn check_receipt_data_len(
241 receipt_data: &[ReceiptGasInfoV1],
242 extrinsics_len: usize,
243) -> Result<(), ClientError> {
244 if receipt_data.len() != extrinsics_len {
245 log::error!(
246 target: LOG_TARGET,
247 "Receipt data length ({}) does not match extrinsics length ({extrinsics_len})",
248 receipt_data.len(),
249 );
250 return Err(ClientError::ReceiptDataLengthMismatch);
251 }
252
253 Ok(())
254}
255
256fn select_committed_outside_frame_logs(
271 logs: Vec<Log>,
272 log_event_indices: &[u32],
273 substrate_block_number: SubstrateBlockNumber,
274 first_log_index: u32,
275) -> Vec<Log> {
276 let mut by_event_index: HashMap<U256, Log> =
277 logs.into_iter().map(|log| (log.log_index, log)).collect();
278 log_event_indices
279 .iter()
280 .filter_map(|event_index| {
281 let log = by_event_index.remove(&U256::from(*event_index));
282 if log.is_none() {
283 log::warn!(
284 target: LOG_TARGET,
285 "Block #{substrate_block_number} committed the log at event {event_index} to its \
286 synthetic transaction but no such log decoded from its events",
287 );
288 }
289 log
290 })
291 .enumerate()
292 .map(|(position, mut log)| {
293 log.log_index = U256::from(first_log_index as u64 + position as u64);
294 log
295 })
296 .collect()
297}
298
299enum ReceiptData {
301 Available { receipt_data: Vec<ReceiptGasInfoV1>, synthetic: Option<SyntheticTransactionV1> },
304 Unsupported,
309 Failed,
311}
312
313type FetchReceiptDataFn =
314 Arc<dyn Fn(SubstrateBlock) -> Pin<Box<dyn Future<Output = ReceiptData> + Send>> + Send + Sync>;
315
316type FetchEthBlockHashFn = Arc<
317 dyn Fn(H256, SubstrateBlockNumber) -> Pin<Box<dyn Future<Output = Option<H256>> + Send>>
318 + Send
319 + Sync,
320>;
321
322type RecoverEthAddressFn = Arc<dyn Fn(&TransactionSigned) -> Result<H160, ()> + Send + Sync>;
323
324#[derive(Clone)]
326pub struct ReceiptExtractor {
327 fetch_receipt_data: FetchReceiptDataFn,
329
330 fetch_eth_block_hash: FetchEthBlockHashFn,
332
333 first_evm_block: Arc<AtomicU64>,
337
338 recover_eth_address: RecoverEthAddressFn,
340
341 chain_id: u64,
344}
345
346impl ReceiptExtractor {
347 pub fn new(runtime_api_provider: VersionAwareRuntimeApiProvider, chain_id: u64) -> Self {
351 Self::new_with_custom_address_recovery(
352 runtime_api_provider,
353 chain_id,
354 Arc::new(|signed_tx: &TransactionSigned| signed_tx.recover_eth_address()),
355 )
356 }
357
358 pub fn new_with_custom_address_recovery(
363 runtime_api_provider: VersionAwareRuntimeApiProvider,
364 chain_id: u64,
365 recover_eth_address_fn: RecoverEthAddressFn,
366 ) -> Self {
367 let provider = runtime_api_provider.clone();
368 let fetch_eth_block_hash = Arc::new(move |substrate_block_hash, substrate_block_number| {
369 let provider = provider.clone();
370
371 let fut = async move {
372 let runtime_api = provider
373 .at_block_hash_and_number(substrate_block_hash, substrate_block_number)
374 .await
375 .inspect_err(|err| {
376 log::debug!(
377 target: LOG_TARGET,
378 "Failed to access the runtime API at block #{substrate_block_number} \
379 ({substrate_block_hash:?}) for an eth_block_hash query: {err:?}"
380 );
381 })
382 .ok()?;
383 runtime_api
384 .eth_block_hash(U256::from(substrate_block_number))?
385 .await
386 .inspect_err(|err| {
387 log::debug!(
388 target: LOG_TARGET,
389 "Failed to query eth_block_hash at block #{substrate_block_number} \
390 ({substrate_block_hash:?}): {err:?}"
391 );
392 })
393 .ok()
394 .flatten()
395 };
396
397 Box::pin(fut) as Pin<Box<_>>
398 });
399
400 let provider = runtime_api_provider;
401 let fetch_receipt_data = Arc::new(move |at_block: SubstrateBlock| {
402 let provider = provider.clone();
403
404 let fut = async move {
405 let block_hash = at_block.block_hash();
406 let runtime_api = match provider.at_resolved_block(at_block).await {
407 Ok(api) => api,
408 Err(err) => {
409 log::debug!(
410 target: LOG_TARGET,
411 "Failed to access the runtime API at block {block_hash:?} for an \
412 eth_receipt_data query: {err:?}"
413 );
414 return ReceiptData::Failed;
415 },
416 };
417 let Some(query) = runtime_api.eth_receipt_data() else {
418 return ReceiptData::Unsupported;
419 };
420 match query.await {
421 Ok((receipt_data, synthetic)) => {
422 ReceiptData::Available { receipt_data, synthetic }
423 },
424 Err(err) => {
425 log::debug!(
426 target: LOG_TARGET,
427 "Failed to query eth_receipt_data at block {block_hash:?}: {err:?}"
428 );
429 ReceiptData::Failed
430 },
431 }
432 };
433
434 Box::pin(fut) as Pin<Box<_>>
435 });
436
437 Self {
438 fetch_receipt_data,
439 fetch_eth_block_hash,
440 first_evm_block: Arc::new(AtomicU64::new(u64::MAX)),
441 recover_eth_address: recover_eth_address_fn,
442 chain_id,
443 }
444 }
445
446 #[cfg(test)]
447 pub fn new_mock() -> Self {
448 let fetch_receipt_data =
449 Arc::new(|_| Box::pin(std::future::ready(ReceiptData::Unsupported)) as Pin<Box<_>>);
450 let fetch_eth_block_hash =
452 Arc::new(|block_hash: H256, block_number: SubstrateBlockNumber| {
453 let bytes: Vec<u8> = [block_hash.as_bytes(), &block_number.to_be_bytes()].concat();
455 let eth_block_hash = H256::from(keccak_256(&bytes));
456 Box::pin(std::future::ready(Some(eth_block_hash))) as Pin<Box<_>>
457 });
458
459 Self {
460 fetch_receipt_data,
461 fetch_eth_block_hash,
462 first_evm_block: Arc::new(AtomicU64::new(u64::MAX)),
463 recover_eth_address: Arc::new(|signed_tx: &TransactionSigned| {
464 signed_tx.recover_eth_address()
465 }),
466 chain_id: 420_420_420,
467 }
468 }
469
470 pub fn is_before_first_evm_block(&self, block_number: SubstrateBlockNumber) -> bool {
473 let val = self.first_evm_block.load(Ordering::Acquire);
474 val != u64::MAX && block_number < val
475 }
476
477 pub fn set_first_evm_block(&self, block_number: SubstrateBlockNumber) {
479 let prev = self.first_evm_block.fetch_min(block_number, Ordering::AcqRel);
480 if block_number > prev {
481 log::debug!(target: LOG_TARGET,
482 "Ignored attempt to raise first_evm_block to #{block_number}, current is #{prev}");
483 }
484 }
485
486 pub fn first_evm_block(&self) -> Option<SubstrateBlockNumber> {
488 let val = self.first_evm_block.load(Ordering::Acquire);
489 (val != u64::MAX).then_some(val)
490 }
491
492 async fn resolve_eth_block_hash(
494 &self,
495 substrate_block_hash: H256,
496 substrate_block_number: SubstrateBlockNumber,
497 ) -> H256 {
498 match (self.fetch_eth_block_hash)(substrate_block_hash, substrate_block_number).await {
499 Some(hash) => hash,
500 None => {
501 log::trace!(target: LOG_TARGET,
502 "eth_block_hash returned None for substrate block \
503 #{substrate_block_number} ({substrate_block_hash:?}), \
504 falling back to substrate hash as ETH hash");
505 substrate_block_hash
506 },
507 }
508 }
509
510 fn decode_transaction_and_build_receipt(
512 &self,
513 eth_block_hash: H256,
514 block_number: U256,
515 call: EthTransact,
516 transaction_hash: H256,
517 transaction_index: usize,
518 receipt_gas_info: ReceiptGasInfoV1,
519 reverted: bool,
520 logs: Vec<Log>,
521 ) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
522 let signed_tx =
523 TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
524 let from = (self.recover_eth_address)(&signed_tx).map_err(|_| {
525 log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
526 ClientError::RecoverEthAddressFailed
527 })?;
528
529 let tx_info = GenericTransaction::from_signed(
530 signed_tx.clone(),
531 receipt_gas_info.effective_gas_price,
532 Some(from),
533 );
534
535 let contract_address = if tx_info.to.is_none() {
536 Some(create1(
537 &from,
538 tx_info
539 .nonce
540 .unwrap_or_default()
541 .try_into()
542 .map_err(|_| ClientError::ConversionFailed)?,
543 ))
544 } else {
545 None
546 };
547
548 let receipt = ReceiptInfo::new(
549 eth_block_hash,
550 block_number,
551 contract_address,
552 from,
553 logs,
554 tx_info.to,
555 receipt_gas_info.effective_gas_price,
556 U256::from(receipt_gas_info.gas_used),
557 !reverted,
558 transaction_hash,
559 transaction_index.into(),
560 tx_info.r#type.unwrap_or_default(),
561 );
562 Ok((signed_tx, receipt))
563 }
564
565 fn synthetic_tx(&self, eth_block_number: U256) -> (Vec<u8>, H256) {
569 let payload = pallet_revive::evm::synthetic_log_transaction(
570 eth_block_number,
571 U256::from(self.chain_id),
572 );
573 let hash = H256(keccak_256(&payload));
574 (payload, hash)
575 }
576
577 fn build_synthetic_receipt(
581 &self,
582 eth_block_hash: H256,
583 eth_block_number: U256,
584 transaction_index: usize,
585 gas_info: ReceiptGasInfoV1,
586 logs: Vec<Log>,
587 ) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
588 let (payload, transaction_hash) = self.synthetic_tx(eth_block_number);
589 let signed_tx =
590 TransactionSigned::decode(&payload).map_err(|_| ClientError::TxDecodingFailed)?;
591 let receipt = ReceiptInfo::new(
592 eth_block_hash,
593 eth_block_number,
594 None,
595 H160::zero(),
596 logs,
597 None,
598 gas_info.effective_gas_price,
599 U256::from(gas_info.gas_used),
600 true,
601 transaction_hash,
602 transaction_index.into(),
603 Default::default(),
604 );
605 Ok((signed_tx, receipt))
606 }
607
608 pub async fn extract_from_block(
610 &self,
611 block: &SubstrateBlock,
612 ) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
613 let eth_block_hash =
614 self.resolve_eth_block_hash(block.block_hash(), block.block_number()).await;
615
616 self.extract_from_block_with_eth_hash(block, eth_block_hash).await
617 }
618
619 pub async fn extract_from_block_with_eth_hash(
623 &self,
624 block: &SubstrateBlock,
625 eth_block_hash: H256,
626 ) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
627 if self.is_before_first_evm_block(block.block_number()) {
628 return Ok(vec![]);
629 }
630
631 let (extrinsics, synthetic, synthetic_tx_index) = self.get_block_extrinsics(block).await?;
632 let eth_tx_by_index: BTreeMap<usize, (EthTransact, H256, ReceiptGasInfoV1)> = extrinsics
633 .into_iter()
634 .map(|(call, receipt_gas_info, extrinsic_index)| {
635 let hash = H256(keccak_256(&call.payload));
636 (extrinsic_index, (call, hash, receipt_gas_info))
637 })
638 .collect();
639
640 if eth_tx_by_index.is_empty() && synthetic.is_none() {
643 return Ok(vec![]);
644 }
645
646 let substrate_block_number = block.block_number();
647 let eth_block_number: U256 = substrate_block_number.into();
648 let (_, synthetic_tx_hash) = self.synthetic_tx(eth_block_number);
649 let block_events = block.events().fetch().await.inspect_err(|err| {
650 log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
651 })?;
652 let (reverted_extrinsics, mut logs_by_extrinsic, outside_frame_logs) =
653 extract_revive_events(
654 &block_events,
655 substrate_block_number,
656 eth_block_number,
657 eth_block_hash,
658 |idx| eth_tx_by_index.get(&idx).map(|(_, hash, _)| *hash),
659 synthetic_tx_hash,
660 synthetic_tx_index,
661 );
662
663 let mut receipts: Vec<_> = eth_tx_by_index
664 .into_iter()
665 .map(|(transaction_index, (call, transaction_hash, receipt_gas_info))| {
666 let reverted = reverted_extrinsics.contains(&transaction_index);
667 let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
668 self.decode_transaction_and_build_receipt(
669 eth_block_hash,
670 eth_block_number,
671 call,
672 transaction_hash,
673 transaction_index,
674 receipt_gas_info,
675 reverted,
676 logs,
677 )
678 .inspect_err(|err| {
679 log::warn!(target: LOG_TARGET, "Error extracting extrinsic: {err:?}");
680 })
681 })
682 .collect::<Result<Vec<_>, _>>()?;
683
684 if let Some(synthetic) = synthetic {
689 let logs = select_committed_outside_frame_logs(
690 outside_frame_logs,
691 &synthetic.log_event_indices,
692 substrate_block_number,
693 block_events.len(),
694 );
695
696 receipts.push(self.build_synthetic_receipt(
697 eth_block_hash,
698 eth_block_number,
699 synthetic_tx_index,
700 synthetic.gas_info,
701 logs,
702 )?);
703 }
704
705 Ok(receipts)
706 }
707
708 async fn get_block_extrinsics(
714 &self,
715 block: &SubstrateBlock,
716 ) -> Result<
717 (Vec<(EthTransact, ReceiptGasInfoV1, usize)>, Option<SyntheticTransactionV1>, usize),
718 ClientError,
719 > {
720 let block_extrinsics = block.extrinsics().fetch().await.inspect_err(|err| {
721 log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.block_number());
722 })?;
723
724 let block_number = block.block_number();
725 let extrinsics = extract_eth_transacts(&block_extrinsics, block_number)?;
726 let synthetic_tx_index = synthetic_tx_index(&block_extrinsics);
727
728 let (receipt_data, synthetic) = match (self.fetch_receipt_data)(block.clone()).await {
731 ReceiptData::Available { receipt_data, synthetic } => (receipt_data, synthetic),
732 ReceiptData::Unsupported => (Vec::new(), None),
735 ReceiptData::Failed => {
736 log::trace!(target: LOG_TARGET,
737 "Receipt data not found for block #{} ({:?})",
738 block.block_number(), block.block_hash());
739 return Err(ClientError::ReceiptDataNotFound);
740 },
741 };
742
743 check_receipt_data_len(&receipt_data, extrinsics.len())?;
744
745 Ok((
746 extrinsics
747 .into_iter()
748 .zip(receipt_data)
749 .map(|((call, ext_idx), rec)| (call, rec, ext_idx))
750 .collect(),
751 synthetic,
752 synthetic_tx_index,
753 ))
754 }
755
756 pub async fn extract_from_transaction(
759 &self,
760 block: &SubstrateBlock,
761 transaction_index: usize,
762 ) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
763 let (extrinsics, synthetic, synthetic_tx_index) = self.get_block_extrinsics(block).await?;
764 let mut eth_tx_by_index: BTreeMap<usize, (EthTransact, H256, ReceiptGasInfoV1)> =
765 extrinsics
766 .into_iter()
767 .map(|(call, receipt_gas_info, extrinsic_index)| {
768 let hash = H256(keccak_256(&call.payload));
769 (extrinsic_index, (call, hash, receipt_gas_info))
770 })
771 .collect();
772
773 let is_synthetic = transaction_index == synthetic_tx_index && synthetic.is_some();
774
775 if !eth_tx_by_index.contains_key(&transaction_index) && !is_synthetic {
776 log::trace!(target: LOG_TARGET,
777 "extract_from_transaction: no EVM extrinsic at tx_index {transaction_index} \
778 in block #{} ({:?})", block.block_number(), block.block_hash());
779 return Err(ClientError::EthExtrinsicNotFound);
780 }
781
782 let substrate_block_number = block.block_number();
783 let eth_block_number: U256 = substrate_block_number.into();
784 let eth_block_hash =
785 self.resolve_eth_block_hash(block.block_hash(), substrate_block_number).await;
786 let (_, synthetic_tx_hash) = self.synthetic_tx(eth_block_number);
787 let block_events = block.events().fetch().await.inspect_err(|err| {
788 log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
789 })?;
790 let (reverted_extrinsics, mut logs_by_extrinsic, outside_frame_logs) =
791 extract_revive_events(
792 &block_events,
793 substrate_block_number,
794 eth_block_number,
795 eth_block_hash,
796 |idx| eth_tx_by_index.get(&idx).map(|(_, hash, _)| *hash),
797 synthetic_tx_hash,
798 synthetic_tx_index,
799 );
800
801 if is_synthetic {
802 let synthetic = synthetic.expect("is_synthetic implies Some; qed");
803 let logs = select_committed_outside_frame_logs(
804 outside_frame_logs,
805 &synthetic.log_event_indices,
806 substrate_block_number,
807 block_events.len(),
808 );
809 return self.build_synthetic_receipt(
810 eth_block_hash,
811 eth_block_number,
812 synthetic_tx_index,
813 synthetic.gas_info,
814 logs,
815 );
816 }
817
818 let (eth_call, transaction_hash, receipt_gas_info) =
819 eth_tx_by_index.remove(&transaction_index).expect("presence checked above; qed");
820 let reverted = reverted_extrinsics.contains(&transaction_index);
821 let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
822 self.decode_transaction_and_build_receipt(
823 eth_block_hash,
824 eth_block_number,
825 eth_call,
826 transaction_hash,
827 transaction_index,
828 receipt_gas_info,
829 reverted,
830 logs,
831 )
832 }
833
834 pub async fn get_ethereum_block_hash(
836 &self,
837 block_hash: &H256,
838 block_number: SubstrateBlockNumber,
839 ) -> Option<H256> {
840 (self.fetch_eth_block_hash)(*block_hash, block_number).await
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 use pallet_revive::evm::{Account, TransactionLegacyUnsigned, TransactionUnsigned};
849
850 fn signed_call(account: &Account, tx: TransactionUnsigned) -> (EthTransact, H256) {
851 let payload = account.sign_transaction(tx).signed_payload();
852 let hash = H256(keccak_256(&payload));
853 (EthTransact { payload }, hash)
854 }
855
856 fn legacy_call_tx(to: H160) -> TransactionUnsigned {
857 TransactionUnsigned::from(TransactionLegacyUnsigned {
858 chain_id: Some(U256::from(1)),
859 to: Some(to),
860 gas: U256::from(21_000),
861 ..Default::default()
862 })
863 }
864
865 fn gas_info() -> ReceiptGasInfoV1 {
866 ReceiptGasInfoV1 {
867 gas_used: U256::from(21_000),
868 effective_gas_price: U256::from(1_000_000_000),
869 }
870 }
871
872 #[test]
873 fn build_receipt_for_call() {
874 let extractor = ReceiptExtractor::new_mock();
875 let account = Account::default();
876 let eth_block_hash = H256::from([0xAB; 32]);
877 let block_number = U256::from(42);
878 let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
879
880 let (signed_tx, receipt) = extractor
882 .decode_transaction_and_build_receipt(
883 eth_block_hash,
884 block_number,
885 call,
886 tx_hash,
887 3,
888 gas_info(),
889 false,
890 vec![],
891 )
892 .unwrap();
893
894 assert!(receipt.is_success());
895 assert_eq!(receipt.from, account.address());
896 assert_eq!(receipt.to, Some(account.address()));
897 assert_eq!(receipt.contract_address, None);
898 assert_eq!(receipt.block_hash, eth_block_hash);
899 assert_eq!(receipt.block_number, block_number);
900 assert_eq!(receipt.transaction_hash, tx_hash);
901 assert_eq!(receipt.transaction_index, U256::from(3));
902 assert_eq!(receipt.gas_used, U256::from(21_000));
903 assert_eq!(signed_tx.recover_eth_address().unwrap(), account.address());
904
905 let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
907 let (_, receipt) = extractor
908 .decode_transaction_and_build_receipt(
909 eth_block_hash,
910 block_number,
911 call,
912 tx_hash,
913 3,
914 gas_info(),
915 true,
916 vec![],
917 )
918 .unwrap();
919
920 assert!(!receipt.is_success());
921 assert_eq!(receipt.from, account.address());
922 }
923
924 #[test]
925 fn build_receipt_for_deploy() {
926 let extractor = ReceiptExtractor::new_mock();
927 let account = Account::default();
928 let deploy_tx = TransactionUnsigned::from(TransactionLegacyUnsigned {
929 chain_id: Some(U256::from(1)),
930 gas: U256::from(100_000),
931 nonce: U256::from(0),
932 ..Default::default()
933 });
934 let (call, tx_hash) = signed_call(&account, deploy_tx);
935
936 let (_, receipt) = extractor
937 .decode_transaction_and_build_receipt(
938 H256::zero(),
939 U256::from(1),
940 call,
941 tx_hash,
942 0,
943 gas_info(),
944 false,
945 vec![],
946 )
947 .unwrap();
948
949 assert!(receipt.is_success());
950 assert_eq!(receipt.to, None);
951 assert_eq!(receipt.contract_address, Some(create1(&account.address(), 0)));
952 assert_eq!(receipt.from, account.address());
953 }
954
955 #[test]
956 fn build_receipt_rejects_invalid_payload() {
957 let extractor = ReceiptExtractor::new_mock();
958
959 let call = EthTransact { payload: vec![0xde, 0xad] };
961 let hash = H256(keccak_256(&call.payload));
962 let err = extractor
963 .decode_transaction_and_build_receipt(
964 H256::zero(),
965 U256::from(1),
966 call,
967 hash,
968 0,
969 gas_info(),
970 false,
971 vec![],
972 )
973 .unwrap_err();
974 assert!(matches!(err, ClientError::TxDecodingFailed));
975
976 let extractor = ReceiptExtractor {
978 recover_eth_address: Arc::new(|_| Err(())),
979 ..ReceiptExtractor::new_mock()
980 };
981 let account = Account::default();
982 let (call, hash) = signed_call(&account, legacy_call_tx(account.address()));
983 let err = extractor
984 .decode_transaction_and_build_receipt(
985 H256::zero(),
986 U256::from(1),
987 call,
988 hash,
989 0,
990 gas_info(),
991 false,
992 vec![],
993 )
994 .unwrap_err();
995 assert!(matches!(err, ClientError::RecoverEthAddressFailed));
996 }
997
998 #[test]
999 fn defaults_and_first_evm_block_only_decreases() {
1000 let extractor = ReceiptExtractor::new_mock();
1001
1002 assert!(extractor.first_evm_block().is_none());
1003
1004 extractor.set_first_evm_block(100);
1006 assert_eq!(extractor.first_evm_block(), Some(100));
1007
1008 extractor.set_first_evm_block(50);
1009 assert_eq!(extractor.first_evm_block(), Some(50));
1010
1011 extractor.set_first_evm_block(100);
1013 assert_eq!(extractor.first_evm_block(), Some(50));
1014 }
1015
1016 use crate::block_info_provider::test::chain_config;
1017 use codec::{Compact, Encode};
1018 use frame_system::EventRecord;
1019 use revive_dev_runtime::{Runtime, RuntimeEvent};
1020 use subxt::{PolkadotConfig, client::OfflineClient, events::Events};
1021
1022 fn offline_client() -> OfflineClient<PolkadotConfig> {
1024 OfflineClient::<PolkadotConfig>::new_with_config(chain_config())
1025 }
1026
1027 struct EventsBuilder {
1029 bytes: Vec<u8>,
1030 count: u32,
1031 }
1032
1033 impl EventsBuilder {
1034 fn new() -> Self {
1035 Self { bytes: Vec::new(), count: 0 }
1036 }
1037
1038 fn push_event(
1039 mut self,
1040 phase: frame_system::Phase,
1041 event: pallet_revive::Event<Runtime>,
1042 ) -> Self {
1043 EventRecord::<RuntimeEvent, H256> {
1044 phase,
1045 event: RuntimeEvent::Revive(event),
1046 topics: vec![],
1047 }
1048 .encode_to(&mut self.bytes);
1049 self.count += 1;
1050 self
1051 }
1052
1053 fn encode(self) -> Vec<u8> {
1055 let mut encoded_events = Vec::new();
1056 Compact(self.count).encode_to(&mut encoded_events);
1057 encoded_events.extend(self.bytes);
1058 encoded_events
1059 }
1060
1061 fn build(self) -> Events<SrcChainConfig> {
1062 let client = offline_client();
1063 let at_block =
1064 client.at_block(0u64).expect("spec version range covers all block numbers; qed");
1065 at_block.events().from_bytes(self.encode())
1066 }
1067 }
1068
1069 #[test]
1070 fn extract_revive_events_decodes_contract_emitted_log() {
1071 let contract = H160::from([0x11; 20]);
1072 let topics = vec![H256::from([0x22; 32]), H256::from([0x33; 32])];
1073 let data = vec![0xde, 0xad, 0xbe, 0xef];
1074 let events = EventsBuilder::new()
1075 .push_event(
1076 frame_system::Phase::ApplyExtrinsic(5),
1077 pallet_revive::Event::ContractEmitted {
1078 contract,
1079 data: data.clone(),
1080 topics: topics.clone(),
1081 },
1082 )
1083 .build();
1084
1085 let tx_hash = H256::from([0xAA; 32]);
1086 let eth_block_hash = H256::from([0xBB; 32]);
1087 let substrate_block_number = 42u64;
1088 let eth_block_number = U256::from(substrate_block_number);
1089
1090 let (reverts, logs, outside_frame) = extract_revive_events(
1091 &events,
1092 substrate_block_number,
1093 eth_block_number,
1094 eth_block_hash,
1095 |idx| (idx == 5).then_some(tx_hash),
1096 H256::zero(),
1097 99,
1098 );
1099
1100 assert!(reverts.is_empty());
1101 assert!(outside_frame.is_empty());
1102 assert_eq!(logs.len(), 1);
1103 let log = &logs[&5][0];
1104 assert_eq!(log.address, contract);
1105 assert_eq!(log.topics, topics);
1106 assert_eq!(log.data.as_ref().unwrap().0, data);
1107 assert_eq!(log.block_hash, eth_block_hash);
1108 assert_eq!(log.block_number, eth_block_number);
1109 assert_eq!(log.transaction_hash, tx_hash);
1110 assert_eq!(log.transaction_index, U256::from(5));
1111 }
1112
1113 #[test]
1114 fn extract_revive_events_buckets_non_eth_logs_as_outside_frame() {
1115 let empty_contract_emitted = pallet_revive::Event::ContractEmitted {
1120 contract: H160::zero(),
1121 data: vec![],
1122 topics: vec![],
1123 };
1124 let revert = pallet_revive::Event::EthExtrinsicRevert {
1125 dispatch_error: sp_runtime::DispatchError::Other("skipped-phase revert"),
1126 };
1127 let events = EventsBuilder::new()
1128 .push_event(frame_system::Phase::Finalization, empty_contract_emitted.clone())
1129 .push_event(frame_system::Phase::Initialization, revert.clone())
1130 .push_event(frame_system::Phase::ApplyExtrinsic(5), empty_contract_emitted)
1131 .push_event(frame_system::Phase::ApplyExtrinsic(5), revert)
1132 .build();
1133
1134 let synthetic_hash = H256::from([0x99; 32]);
1135 let (reverts, logs, outside_frame) = extract_revive_events(
1138 &events,
1139 0,
1140 U256::zero(),
1141 H256::zero(),
1142 |idx| (idx == 7).then_some(H256::zero()),
1143 synthetic_hash,
1144 3,
1145 );
1146
1147 assert!(reverts.is_empty());
1148 assert!(logs.is_empty());
1149 assert_eq!(outside_frame.len(), 2, "the hook-phase log and the non-eth extrinsic's log");
1150 for log in &outside_frame {
1151 assert_eq!(log.transaction_hash, synthetic_hash);
1152 assert_eq!(log.transaction_index, U256::from(3));
1153 }
1154 }
1155
1156 #[test]
1157 fn extract_revive_events_buckets_hook_phase_logs_as_outside_frame() {
1158 let synthetic_hash = H256::from([0xEE; 32]);
1162 let events = EventsBuilder::new()
1163 .push_event(
1164 frame_system::Phase::Initialization,
1165 pallet_revive::Event::ContractEmitted {
1166 contract: H160::from([0xaa; 20]),
1167 data: vec![],
1168 topics: vec![],
1169 },
1170 )
1171 .push_event(
1172 frame_system::Phase::Finalization,
1173 pallet_revive::Event::ContractEmitted {
1174 contract: H160::from([0xbb; 20]),
1175 data: vec![],
1176 topics: vec![],
1177 },
1178 )
1179 .build();
1180
1181 let (reverts, logs, outside_frame) = extract_revive_events(
1182 &events,
1183 0,
1184 U256::zero(),
1185 H256::zero(),
1186 |_| Some(H256::from([0x77; 32])),
1189 synthetic_hash,
1190 4,
1191 );
1192
1193 assert!(reverts.is_empty());
1194 assert!(logs.is_empty(), "a hook-phase log belongs to no extrinsic");
1195 assert_eq!(outside_frame.len(), 2, "both hook phases are bucketed");
1196 for log in &outside_frame {
1197 assert_eq!(log.transaction_hash, synthetic_hash);
1198 assert_eq!(log.transaction_index, U256::from(4));
1199 }
1200 }
1201
1202 #[test]
1203 fn extract_revive_events_accumulates_per_extrinsic() {
1204 let tx0 = H256::from([0x01; 32]);
1205 let tx1 = H256::from([0x02; 32]);
1206 let tx2 = H256::from([0x03; 32]);
1207 let emitted_by = |contract: H160| pallet_revive::Event::ContractEmitted {
1208 contract,
1209 data: vec![],
1210 topics: vec![],
1211 };
1212 let events = EventsBuilder::new()
1213 .push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xaa; 20])))
1214 .push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xbb; 20])))
1215 .push_event(
1216 frame_system::Phase::ApplyExtrinsic(1),
1217 pallet_revive::Event::EthExtrinsicRevert {
1218 dispatch_error: sp_runtime::DispatchError::Other("tx-1 revert"),
1219 },
1220 )
1221 .push_event(frame_system::Phase::ApplyExtrinsic(2), emitted_by(H160::from([0xcc; 20])))
1222 .build();
1223
1224 let (reverts, logs, outside_frame) = extract_revive_events(
1225 &events,
1226 0,
1227 U256::zero(),
1228 H256::zero(),
1229 |idx| match idx {
1230 0 => Some(tx0),
1231 1 => Some(tx1),
1232 2 => Some(tx2),
1233 _ => None,
1234 },
1235 H256::zero(),
1236 3,
1237 );
1238
1239 assert!(outside_frame.is_empty());
1240 assert_eq!(reverts, [1usize].into_iter().collect::<HashSet<_>>());
1241 assert_eq!(logs[&0].len(), 2);
1242 assert_eq!(logs[&2].len(), 1);
1243 assert_eq!(logs[&0][0].log_index, U256::from(0));
1245 assert_eq!(logs[&0][1].log_index, U256::from(1));
1246 assert_eq!(logs[&2][0].log_index, U256::from(3));
1247 }
1248
1249 const ETH_TRANSACT_PAYLOAD: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
1250
1251 fn encode_bare(call: revive_dev_runtime::RuntimeCall) -> Vec<u8> {
1253 let extrinsic: revive_dev_runtime::UncheckedExtrinsic =
1254 pallet_revive::evm::runtime::UncheckedExtrinsic(
1255 sp_runtime::generic::UncheckedExtrinsic::new_bare(call),
1256 );
1257 extrinsic.encode()
1258 }
1259
1260 fn eth_transact_extrinsic() -> Vec<u8> {
1261 encode_bare(revive_dev_runtime::RuntimeCall::Revive(pallet_revive::Call::eth_transact {
1262 payload: ETH_TRANSACT_PAYLOAD.to_vec(),
1263 }))
1264 }
1265
1266 fn non_revive_extrinsic() -> Vec<u8> {
1267 encode_bare(revive_dev_runtime::RuntimeCall::System(frame_system::Call::remark {
1268 remark: vec![0x01],
1269 }))
1270 }
1271
1272 async fn extract_from(blobs: Vec<Vec<u8>>) -> Result<Vec<(EthTransact, usize)>, ClientError> {
1274 const BLOCK_NUMBER: SubstrateBlockNumber = 42;
1275
1276 let client = offline_client();
1277 let at_block = client
1278 .at_block(BLOCK_NUMBER)
1279 .expect("spec version range covers every block number; qed");
1280 let extrinsics = at_block.extrinsics().from_bytes(blobs).await;
1281 extract_eth_transacts(&extrinsics, BLOCK_NUMBER)
1282 }
1283
1284 #[tokio::test]
1285 async fn synthetic_tx_index_is_one_past_the_last_extrinsic() {
1286 let blobs = vec![eth_transact_extrinsic(), non_revive_extrinsic(), vec![0xff; 4]];
1289
1290 let client = offline_client();
1291 let at_block = client
1292 .at_block(42u64)
1293 .expect("spec version range covers every block number; qed");
1294 let extrinsics = at_block.extrinsics().from_bytes(blobs).await;
1295
1296 let eth = extract_eth_transacts(&extrinsics, 42).unwrap();
1297 assert_eq!(eth.iter().map(|(_, idx)| *idx).max(), Some(0));
1298 assert_eq!(synthetic_tx_index(&extrinsics), 3, "past the undecodable extrinsic too");
1299 }
1300
1301 #[tokio::test]
1302 async fn extract_eth_transacts_collects_revive_calls() {
1303 let calls = extract_from(vec![non_revive_extrinsic(), eth_transact_extrinsic()])
1304 .await
1305 .unwrap();
1306
1307 assert_eq!(calls.len(), 1, "only the revive extrinsic is collected");
1308 assert_eq!(calls[0].1, 1, "the extrinsic index is preserved");
1309 assert_eq!(calls[0].0.payload, ETH_TRANSACT_PAYLOAD, "the call fields are decoded");
1310 }
1311
1312 #[tokio::test]
1313 async fn extract_eth_transacts_keeps_revive_calls_next_to_an_undecodable_one() {
1314 let calls = extract_from(vec![vec![0xff; 4], eth_transact_extrinsic()]).await.unwrap();
1315
1316 assert_eq!(calls.len(), 1, "an undecodable extrinsic must not hide a decoded one");
1317 assert_eq!(calls[0].1, 1, "the extrinsic index is preserved");
1318 assert_eq!(calls[0].0.payload, ETH_TRANSACT_PAYLOAD, "the call fields are decoded");
1319 }
1320
1321 fn gas_infos(n: usize) -> Vec<ReceiptGasInfoV1> {
1323 (0..n)
1324 .map(|i| ReceiptGasInfoV1 {
1325 gas_used: U256::from(i),
1326 effective_gas_price: U256::from(1_000_000_000u64),
1327 })
1328 .collect()
1329 }
1330
1331 #[test]
1332 fn receipt_data_is_reconciled_against_the_block_body() {
1333 check_receipt_data_len(&gas_infos(2), 2).unwrap();
1336
1337 let err = check_receipt_data_len(&gas_infos(3), 2).unwrap_err();
1341 assert!(matches!(err, ClientError::ReceiptDataLengthMismatch));
1342 }
1343
1344 fn outside_frame_logs(event_indices: &[u64]) -> Vec<Log> {
1346 event_indices
1347 .iter()
1348 .map(|i| Log {
1349 address: H160::from_low_u64_be(*i),
1350 log_index: U256::from(*i),
1351 ..Default::default()
1352 })
1353 .collect()
1354 }
1355
1356 fn served_logs(event_indices: &[u64], first_log_index: u64) -> Vec<Log> {
1359 outside_frame_logs(event_indices)
1360 .into_iter()
1361 .zip(first_log_index..)
1362 .map(|(log, log_index)| Log { log_index: U256::from(log_index), ..log })
1363 .collect()
1364 }
1365
1366 #[test]
1367 fn outside_frame_logs_are_served_as_emitted_when_they_all_fitted() {
1368 let logs =
1369 select_committed_outside_frame_logs(outside_frame_logs(&[0, 1, 2]), &[0, 1, 2], 1, 3);
1370
1371 assert_eq!(logs, served_logs(&[0, 1, 2], 3));
1372 }
1373
1374 #[test]
1375 fn outside_frame_logs_the_block_did_not_commit_are_not_served() {
1376 let logs = select_committed_outside_frame_logs(
1380 outside_frame_logs(&[0, 1, 2, 3, 4]),
1381 &[1, 3],
1382 1,
1383 5,
1384 );
1385
1386 assert_eq!(logs, served_logs(&[1, 3], 5));
1387 }
1388
1389 #[test]
1390 fn a_committed_log_that_did_not_decode_is_left_out() {
1391 let logs =
1394 select_committed_outside_frame_logs(outside_frame_logs(&[0, 6]), &[0, 4, 6], 1, 7);
1395
1396 assert_eq!(logs, served_logs(&[0, 6], 7));
1397 }
1398
1399 #[test]
1400 fn synthetic_logs_are_numbered_after_the_blocks_other_logs() {
1401 let logs = select_committed_outside_frame_logs(outside_frame_logs(&[0, 9]), &[0, 9], 1, 10);
1406
1407 assert_eq!(logs, served_logs(&[0, 9], 10));
1408 }
1409
1410 #[test]
1411 fn synthetic_receipt_serves_the_log_the_block_committed() {
1412 let contract = H160::from([0xc0; 20]);
1422 let asset_precompile = H160::from([0xa5; 20]);
1423 let emitted_by = |address: H160| pallet_revive::Event::ContractEmitted {
1424 contract: address,
1425 data: vec![],
1426 topics: vec![],
1427 };
1428 let events = EventsBuilder::new()
1429 .push_event(
1430 frame_system::Phase::ApplyExtrinsic(0),
1431 pallet_revive::Event::Instantiated { deployer: H160::zero(), contract },
1432 )
1433 .push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(contract))
1434 .push_event(frame_system::Phase::ApplyExtrinsic(1), emitted_by(asset_precompile))
1435 .build();
1436
1437 let (_, _, outside_frame) = extract_revive_events(
1438 &events,
1439 1,
1440 U256::one(),
1441 H256::zero(),
1442 |_| None,
1443 H256::from([0x99; 32]),
1444 2,
1445 );
1446 let logs = select_committed_outside_frame_logs(outside_frame, &[2], 1, 3);
1447
1448 assert_eq!(logs.len(), 1, "the one committed log is served");
1449 assert_eq!(
1450 logs[0].address, asset_precompile,
1451 "the synthetic receipt must carry the committed mirror log, not the contract's"
1452 );
1453 }
1454
1455 use subxt::{
1456 OnlineClient,
1457 backend::LegacyBackend,
1458 config::substrate::SubstrateHeader,
1459 rpcs::{
1460 RpcClient,
1461 client::{MockRpcClient, mock_rpc_client::Json},
1462 },
1463 };
1464
1465 async fn mocked_block(extrinsics: Vec<Vec<u8>>, events: Vec<u8>) -> SubstrateBlock {
1468 type Params = Option<Box<serde_json::value::RawValue>>;
1469 let block_hash = H256::repeat_byte(0x2a);
1470 let header = SubstrateHeader::<H256> {
1471 parent_hash: H256::zero(),
1472 number: 42,
1473 state_root: H256::zero(),
1474 extrinsics_root: H256::zero(),
1475 digest: Default::default(),
1476 };
1477 let hex = |bytes: &[u8]| format!("0x{}", hex::encode(bytes));
1478 let body = serde_json::json!({
1479 "block": {
1480 "header": header,
1481 "extrinsics": extrinsics.iter().map(|extrinsic| hex(extrinsic)).collect::<Vec<_>>(),
1482 },
1483 "justifications": null,
1484 });
1485 let events = hex(&events);
1486
1487 let mock =
1488 MockRpcClient::builder()
1489 .method_handler("chain_getBlockHash", move |_: Params| async move {
1490 Json(Some(block_hash))
1491 })
1492 .method_handler("chain_getFinalizedHead", move |_: Params| async move {
1493 Json(block_hash)
1494 })
1495 .method_handler("chain_getHeader", move |_: Params| {
1496 let header = header.clone();
1497 async move { Json(Some(header)) }
1498 })
1499 .method_handler("chain_getBlock", move |_: Params| {
1500 let body = body.clone();
1501 async move { Json(Some(body)) }
1502 })
1503 .method_handler("state_getStorage", move |_: Params| {
1504 let events = events.clone();
1505 async move { Json(Some(events)) }
1506 })
1507 .build();
1508 let backend = LegacyBackend::<SrcChainConfig>::builder().build(RpcClient::new(mock));
1509 let api = OnlineClient::<SrcChainConfig>::from_backend_with_config(
1510 chain_config(),
1511 Arc::new(backend),
1512 )
1513 .await
1514 .unwrap();
1515 api.at_block(block_hash).await.unwrap()
1516 }
1517
1518 #[tokio::test]
1519 async fn synthetic_receipt_is_served_alike_on_both_paths_with_its_logs_numbered_last() {
1520 let account = Account::default();
1524 let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
1525 let extrinsics = vec![
1526 encode_bare(revive_dev_runtime::RuntimeCall::Revive(
1527 pallet_revive::Call::eth_transact { payload: call.payload },
1528 )),
1529 non_revive_extrinsic(),
1530 ];
1531 let hook_mirror = H160::from([0xa1; 20]);
1532 let contract = H160::from([0xc0; 20]);
1533 let uncommitted = H160::from([0xb1; 20]);
1534 let extrinsic_mirror = H160::from([0xb2; 20]);
1535 let emitted_by = |address: H160| pallet_revive::Event::ContractEmitted {
1536 contract: address,
1537 data: vec![],
1538 topics: vec![],
1539 };
1540 let events = EventsBuilder::new()
1541 .push_event(frame_system::Phase::Initialization, emitted_by(hook_mirror))
1542 .push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(contract))
1543 .push_event(frame_system::Phase::ApplyExtrinsic(1), emitted_by(uncommitted))
1544 .push_event(frame_system::Phase::ApplyExtrinsic(1), emitted_by(extrinsic_mirror))
1545 .encode();
1546 let block = mocked_block(extrinsics, events).await;
1547
1548 let extractor = ReceiptExtractor {
1549 fetch_receipt_data: Arc::new(|_| {
1550 Box::pin(std::future::ready(ReceiptData::Available {
1551 receipt_data: vec![gas_info()],
1552 synthetic: Some(SyntheticTransactionV1 {
1553 gas_info: gas_info(),
1554 log_event_indices: vec![0, 3],
1555 }),
1556 })) as Pin<Box<_>>
1557 }),
1558 ..ReceiptExtractor::new_mock()
1559 };
1560 let eth_block_hash = extractor
1561 .get_ethereum_block_hash(&block.block_hash(), block.block_number())
1562 .await
1563 .unwrap();
1564
1565 let receipts = extractor
1566 .extract_from_block_with_eth_hash(&block, eth_block_hash)
1567 .await
1568 .unwrap();
1569 let [(_, eth_receipt), (_, synthetic_receipt)] = receipts.as_slice() else {
1570 panic!("one ethereum transaction and the synthetic one, got {}", receipts.len());
1571 };
1572 assert_eq!(eth_receipt.transaction_hash, tx_hash);
1573 assert_eq!(eth_receipt.transaction_index, U256::zero());
1574 assert_eq!(eth_receipt.logs.len(), 1);
1575 assert_eq!(synthetic_receipt.transaction_index, U256::from(2));
1576 assert_eq!(
1577 synthetic_receipt.logs.iter().map(|log| log.address).collect::<Vec<_>>(),
1578 vec![hook_mirror, extrinsic_mirror],
1579 "the committed logs, in the runtime's order"
1580 );
1581 assert_eq!(
1582 synthetic_receipt.logs.iter().map(|log| log.log_index).collect::<Vec<_>>(),
1583 vec![U256::from(4), U256::from(5)],
1584 "numbered from the block's event count, not by event index"
1585 );
1586 assert!(
1587 eth_receipt.logs[0].log_index < synthetic_receipt.logs[0].log_index,
1588 "log_index follows transaction order"
1589 );
1590
1591 let (_, single) = extractor.extract_from_transaction(&block, 2).await.unwrap();
1592 assert_eq!(&single, synthetic_receipt, "the single-receipt path serves the same receipt");
1593 }
1594
1595 fn extractor_answering(receipt_data: fn() -> ReceiptData) -> ReceiptExtractor {
1597 ReceiptExtractor {
1598 fetch_receipt_data: Arc::new(move |_| {
1599 Box::pin(std::future::ready(receipt_data())) as Pin<Box<_>>
1600 }),
1601 ..ReceiptExtractor::new_mock()
1602 }
1603 }
1604
1605 #[tokio::test]
1606 async fn a_failed_receipt_data_query_is_an_error_on_both_paths() {
1607 let block = mocked_block(vec![non_revive_extrinsic()], EventsBuilder::new().encode()).await;
1612 let extractor = extractor_answering(|| ReceiptData::Failed);
1613
1614 let err = extractor
1615 .extract_from_block_with_eth_hash(&block, H256::zero())
1616 .await
1617 .unwrap_err();
1618 assert!(matches!(err, ClientError::ReceiptDataNotFound), "{err:?}");
1619 let err = extractor.extract_from_transaction(&block, 0).await.unwrap_err();
1620 assert!(matches!(err, ClientError::ReceiptDataNotFound), "{err:?}");
1621 }
1622
1623 #[tokio::test]
1624 async fn a_block_before_the_receipt_data_api_is_an_empty_block_on_both_paths() {
1625 let block = mocked_block(vec![non_revive_extrinsic()], EventsBuilder::new().encode()).await;
1628 let extractor = extractor_answering(|| ReceiptData::Unsupported);
1629
1630 let receipts =
1631 extractor.extract_from_block_with_eth_hash(&block, H256::zero()).await.unwrap();
1632 assert!(receipts.is_empty());
1633 let err = extractor.extract_from_transaction(&block, 0).await.unwrap_err();
1634 assert!(matches!(err, ClientError::EthExtrinsicNotFound), "{err:?}");
1635 }
1636}