1use crate::{
18 ClientError, H160, LOG_TARGET,
19 client::{SubstrateBlock, SubstrateBlockNumber, runtime_api::RuntimeApi},
20 subxt_client::{
21 SrcChainConfig,
22 revive::{
23 calls::types::EthTransact,
24 events::{ContractEmitted, EthExtrinsicRevert},
25 },
26 },
27};
28
29use pallet_revive::{
30 create1,
31 evm::{GenericTransaction, H256, Log, ReceiptGasInfo, ReceiptInfo, TransactionSigned, U256},
32};
33use sp_core::keccak_256;
34use std::{
35 collections::{BTreeMap, HashMap, HashSet},
36 future::Future,
37 pin::Pin,
38 sync::{
39 Arc,
40 atomic::{AtomicU32, Ordering},
41 },
42};
43use subxt::{
44 OnlineClient,
45 events::{Phase, StaticEvent},
46};
47
48type EventDetails = subxt::events::EventDetails<SrcChainConfig>;
49
50enum ReviveEvent {
52 Revert,
53 Log(Log),
54}
55
56fn decode_revive_event(
59 event: &EventDetails,
60 block_number: U256,
61 transaction_hash: H256,
62 transaction_index: usize,
63 block_hash: H256,
64) -> Option<ReviveEvent> {
65 if event.pallet_name() != ContractEmitted::PALLET {
66 return None;
67 }
68 if event.variant_name() == EthExtrinsicRevert::EVENT {
69 return Some(ReviveEvent::Revert);
70 }
71 if event.variant_name() == ContractEmitted::EVENT {
72 match event.as_event::<ContractEmitted>().ok().flatten() {
73 Some(evt) => {
74 return Some(ReviveEvent::Log(Log {
75 address: evt.contract,
76 topics: evt.topics,
77 data: Some(evt.data.into()),
78 block_number,
79 transaction_hash,
80 transaction_index: transaction_index.into(),
81 block_hash,
82 log_index: event.index().into(),
83 ..Default::default()
84 }));
85 },
86 None => log::warn!(
87 target: LOG_TARGET,
88 "Failed to decode ContractEmitted event {} in block {block_number} (tx {transaction_hash:?}), log dropped from receipt",
89 event.index()
90 ),
91 }
92 }
93 None
94}
95
96fn extract_revive_events(
106 block_events: &subxt::events::Events<SrcChainConfig>,
107 substrate_block_number: SubstrateBlockNumber,
108 eth_block_number: U256,
109 eth_block_hash: H256,
110 eth_tx_hash_for: impl Fn(usize) -> Option<H256>,
111) -> (HashSet<usize>, HashMap<usize, Vec<Log>>) {
112 let mut reverted_extrinsics: HashSet<usize> = HashSet::new();
113 let mut logs_by_extrinsic: HashMap<usize, Vec<Log>> = HashMap::new();
114
115 for (event_index, event_result) in block_events.iter().enumerate() {
116 let event = match event_result {
117 Ok(e) => e,
118 Err(err) => {
119 log::debug!(
120 target: LOG_TARGET,
121 "Failed to decode event {event_index} in block #{substrate_block_number}: {err:?}"
122 );
123 continue;
124 },
125 };
126
127 let extrinsic_index = match event.phase() {
128 Phase::ApplyExtrinsic(idx) => idx as usize,
129 _ => continue,
130 };
131
132 let Some(eth_tx_hash) = eth_tx_hash_for(extrinsic_index) else { continue };
133
134 match decode_revive_event(
135 &event,
136 eth_block_number,
137 eth_tx_hash,
138 extrinsic_index,
139 eth_block_hash,
140 ) {
141 Some(ReviveEvent::Revert) => {
142 reverted_extrinsics.insert(extrinsic_index);
143 },
144 Some(ReviveEvent::Log(log)) => {
145 logs_by_extrinsic.entry(extrinsic_index).or_default().push(log);
146 },
147 None => {},
148 }
149 }
150
151 (reverted_extrinsics, logs_by_extrinsic)
152}
153
154type FetchReceiptDataFn = Arc<
155 dyn Fn(H256) -> Pin<Box<dyn Future<Output = Option<Vec<ReceiptGasInfo>>> + Send>> + Send + Sync,
156>;
157
158type FetchEthBlockHashFn =
159 Arc<dyn Fn(H256, u64) -> Pin<Box<dyn Future<Output = Option<H256>> + Send>> + Send + Sync>;
160
161type RecoverEthAddressFn = Arc<dyn Fn(&TransactionSigned) -> Result<H160, ()> + Send + Sync>;
162
163#[derive(Clone)]
165pub struct ReceiptExtractor {
166 fetch_receipt_data: FetchReceiptDataFn,
168
169 fetch_eth_block_hash: FetchEthBlockHashFn,
171
172 first_evm_block: Arc<AtomicU32>,
176
177 recover_eth_address: RecoverEthAddressFn,
179}
180
181impl ReceiptExtractor {
182 pub async fn new(api: OnlineClient<SrcChainConfig>) -> Result<Self, ClientError> {
184 Self::new_with_custom_address_recovery(
185 api,
186 Arc::new(|signed_tx: &TransactionSigned| signed_tx.recover_eth_address()),
187 )
188 .await
189 }
190
191 pub async fn new_with_custom_address_recovery(
196 api: OnlineClient<SrcChainConfig>,
197 recover_eth_address_fn: RecoverEthAddressFn,
198 ) -> Result<Self, ClientError> {
199 let api_inner = api.clone();
200 let fetch_eth_block_hash = Arc::new(move |block_hash, block_number| {
201 let api_inner = api_inner.clone();
202
203 let fut = async move {
204 let runtime_api = RuntimeApi::new(api_inner.runtime_api().at(block_hash));
205 runtime_api.eth_block_hash(U256::from(block_number)).await.ok().flatten()
206 };
207
208 Box::pin(fut) as Pin<Box<_>>
209 });
210
211 let api_inner = api.clone();
212 let fetch_receipt_data = Arc::new(move |block_hash| {
213 let api_inner = api_inner.clone();
214
215 let fut = async move {
216 let runtime_api = RuntimeApi::new(api_inner.runtime_api().at(block_hash));
217 runtime_api.eth_receipt_data().await.ok()
218 };
219
220 Box::pin(fut) as Pin<Box<_>>
221 });
222
223 Ok(Self {
224 fetch_receipt_data,
225 fetch_eth_block_hash,
226 first_evm_block: Arc::new(AtomicU32::new(u32::MAX)),
227 recover_eth_address: recover_eth_address_fn,
228 })
229 }
230
231 #[cfg(test)]
232 pub fn new_mock() -> Self {
233 let fetch_receipt_data = Arc::new(|_| Box::pin(std::future::ready(None)) as Pin<Box<_>>);
234 let fetch_eth_block_hash = Arc::new(|block_hash: H256, block_number: u64| {
236 let bytes: Vec<u8> = [block_hash.as_bytes(), &block_number.to_be_bytes()].concat();
238 let eth_block_hash = H256::from(keccak_256(&bytes));
239 Box::pin(std::future::ready(Some(eth_block_hash))) as Pin<Box<_>>
240 });
241
242 Self {
243 fetch_receipt_data,
244 fetch_eth_block_hash,
245 first_evm_block: Arc::new(AtomicU32::new(u32::MAX)),
246 recover_eth_address: Arc::new(|signed_tx: &TransactionSigned| {
247 signed_tx.recover_eth_address()
248 }),
249 }
250 }
251
252 pub fn is_before_first_evm_block(&self, block_number: SubstrateBlockNumber) -> bool {
255 let val = self.first_evm_block.load(Ordering::Acquire);
256 val != u32::MAX && block_number < val
257 }
258
259 pub fn set_first_evm_block(&self, block_number: SubstrateBlockNumber) {
261 let prev = self.first_evm_block.fetch_min(block_number, Ordering::AcqRel);
262 if block_number > prev {
263 log::debug!(target: LOG_TARGET,
264 "Ignored attempt to raise first_evm_block to #{block_number}, current is #{prev}");
265 }
266 }
267
268 pub fn first_evm_block(&self) -> Option<SubstrateBlockNumber> {
270 let val = self.first_evm_block.load(Ordering::Acquire);
271 (val != u32::MAX).then_some(val)
272 }
273
274 async fn resolve_eth_block_hash(
276 &self,
277 substrate_block_hash: H256,
278 substrate_block_number: u64,
279 ) -> H256 {
280 match (self.fetch_eth_block_hash)(substrate_block_hash, substrate_block_number).await {
281 Some(hash) => hash,
282 None => {
283 log::trace!(target: LOG_TARGET,
284 "eth_block_hash returned None for substrate block \
285 #{substrate_block_number} ({substrate_block_hash:?}), \
286 falling back to substrate hash as ETH hash");
287 substrate_block_hash
288 },
289 }
290 }
291
292 fn decode_transaction_and_build_receipt(
294 &self,
295 eth_block_hash: H256,
296 block_number: U256,
297 call: EthTransact,
298 transaction_hash: H256,
299 transaction_index: usize,
300 receipt_gas_info: ReceiptGasInfo,
301 reverted: bool,
302 logs: Vec<Log>,
303 ) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
304 let signed_tx =
305 TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
306 let from = (self.recover_eth_address)(&signed_tx).map_err(|_| {
307 log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
308 ClientError::RecoverEthAddressFailed
309 })?;
310
311 let tx_info = GenericTransaction::from_signed(
312 signed_tx.clone(),
313 receipt_gas_info.effective_gas_price,
314 Some(from),
315 );
316
317 let contract_address = if tx_info.to.is_none() {
318 Some(create1(
319 &from,
320 tx_info
321 .nonce
322 .unwrap_or_default()
323 .try_into()
324 .map_err(|_| ClientError::ConversionFailed)?,
325 ))
326 } else {
327 None
328 };
329
330 let receipt = ReceiptInfo::new(
331 eth_block_hash,
332 block_number,
333 contract_address,
334 from,
335 logs,
336 tx_info.to,
337 receipt_gas_info.effective_gas_price,
338 U256::from(receipt_gas_info.gas_used),
339 !reverted,
340 transaction_hash,
341 transaction_index.into(),
342 tx_info.r#type.unwrap_or_default(),
343 );
344 Ok((signed_tx, receipt))
345 }
346
347 pub async fn extract_from_block(
349 &self,
350 block: &SubstrateBlock,
351 ) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
352 let eth_block_hash = self.resolve_eth_block_hash(block.hash(), block.number() as u64).await;
353
354 self.extract_from_block_with_eth_hash(block, eth_block_hash).await
355 }
356
357 pub async fn extract_from_block_with_eth_hash(
361 &self,
362 block: &SubstrateBlock,
363 eth_block_hash: H256,
364 ) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
365 if self.is_before_first_evm_block(block.number()) {
366 return Ok(vec![]);
367 }
368
369 let eth_tx_by_index: BTreeMap<usize, (EthTransact, H256, ReceiptGasInfo)> = self
370 .get_block_extrinsics(block)
371 .await?
372 .map(|(call, receipt_gas_info, extrinsic_index)| {
373 let hash = H256(keccak_256(&call.payload));
374 (extrinsic_index, (call, hash, receipt_gas_info))
375 })
376 .collect();
377
378 if eth_tx_by_index.is_empty() {
379 return Ok(vec![]);
380 }
381
382 let substrate_block_number = block.number();
383 let eth_block_number: U256 = substrate_block_number.into();
384 let block_events = block.events().await.inspect_err(|err| {
385 log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
386 })?;
387 let (reverted_extrinsics, mut logs_by_extrinsic) = extract_revive_events(
388 &block_events,
389 substrate_block_number,
390 eth_block_number,
391 eth_block_hash,
392 |idx| eth_tx_by_index.get(&idx).map(|(_, hash, _)| *hash),
393 );
394
395 eth_tx_by_index
396 .into_iter()
397 .map(|(transaction_index, (call, transaction_hash, receipt_gas_info))| {
398 let reverted = reverted_extrinsics.contains(&transaction_index);
399 let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
400 self.decode_transaction_and_build_receipt(
401 eth_block_hash,
402 eth_block_number,
403 call,
404 transaction_hash,
405 transaction_index,
406 receipt_gas_info,
407 reverted,
408 logs,
409 )
410 .inspect_err(|err| {
411 log::warn!(target: LOG_TARGET, "Error extracting extrinsic: {err:?}");
412 })
413 })
414 .collect()
415 }
416
417 async fn get_block_extrinsics(
420 &self,
421 block: &SubstrateBlock,
422 ) -> Result<impl Iterator<Item = (EthTransact, ReceiptGasInfo, usize)>, ClientError> {
423 let extrinsics = block.extrinsics().await.inspect_err(|err| {
425 log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.number());
426 })?;
427
428 let receipt_data = (self.fetch_receipt_data)(block.hash()).await.ok_or_else(|| {
429 log::trace!(target: LOG_TARGET,
430 "Receipt data not found for block #{} ({:?})",
431 block.number(), block.hash());
432 ClientError::ReceiptDataNotFound
433 })?;
434 let extrinsics: Vec<_> = extrinsics
435 .iter()
436 .enumerate()
437 .flat_map(|(ext_idx, ext)| {
438 let call = ext.as_extrinsic::<EthTransact>().ok()??;
439 Some((call, ext_idx))
440 })
441 .collect();
442
443 if receipt_data.len() != extrinsics.len() {
445 log::error!(
446 target: LOG_TARGET,
447 "Receipt data length ({}) does not match extrinsics length ({})",
448 receipt_data.len(),
449 extrinsics.len()
450 );
451 Err(ClientError::ReceiptDataLengthMismatch)
452 } else {
453 Ok(extrinsics
454 .into_iter()
455 .zip(receipt_data)
456 .map(|((call, ext_idx), rec)| (call, rec, ext_idx)))
457 }
458 }
459
460 pub async fn extract_from_transaction(
463 &self,
464 block: &SubstrateBlock,
465 transaction_index: usize,
466 ) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
467 let (eth_call, receipt_gas_info, transaction_hash) = self
468 .get_block_extrinsics(block)
469 .await?
470 .find_map(|(call, receipt_gas_info, extrinsic_index)| {
471 (extrinsic_index == transaction_index).then(|| {
472 let hash = H256(keccak_256(&call.payload));
473 (call, receipt_gas_info, hash)
474 })
475 })
476 .ok_or_else(|| {
477 log::trace!(target: LOG_TARGET,
478 "extract_from_transaction: no EVM extrinsic at tx_index {transaction_index} \
479 in block #{} ({:?})", block.number(), block.hash());
480 ClientError::EthExtrinsicNotFound
481 })?;
482
483 let substrate_block_number = block.number();
484 let eth_block_number: U256 = substrate_block_number.into();
485 let eth_block_hash =
486 self.resolve_eth_block_hash(block.hash(), substrate_block_number as u64).await;
487 let block_events = block.events().await.inspect_err(|err| {
488 log::debug!(target: LOG_TARGET, "Error fetching events for block #{substrate_block_number}: {err:?}");
489 })?;
490 let (reverted_extrinsics, mut logs_by_extrinsic) = extract_revive_events(
491 &block_events,
492 substrate_block_number,
493 eth_block_number,
494 eth_block_hash,
495 |idx| (idx == transaction_index).then_some(transaction_hash),
496 );
497
498 let reverted = reverted_extrinsics.contains(&transaction_index);
499 let logs = logs_by_extrinsic.remove(&transaction_index).unwrap_or_default();
500 self.decode_transaction_and_build_receipt(
501 eth_block_hash,
502 eth_block_number,
503 eth_call,
504 transaction_hash,
505 transaction_index,
506 receipt_gas_info,
507 reverted,
508 logs,
509 )
510 }
511
512 pub async fn get_ethereum_block_hash(
514 &self,
515 block_hash: &H256,
516 block_number: u64,
517 ) -> Option<H256> {
518 (self.fetch_eth_block_hash)(*block_hash, block_number).await
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525
526 use pallet_revive::evm::{Account, TransactionLegacyUnsigned, TransactionUnsigned};
527
528 fn signed_call(account: &Account, tx: TransactionUnsigned) -> (EthTransact, H256) {
529 let payload = account.sign_transaction(tx).signed_payload();
530 let hash = H256(keccak_256(&payload));
531 (EthTransact { payload }, hash)
532 }
533
534 fn legacy_call_tx(to: H160) -> TransactionUnsigned {
535 TransactionUnsigned::from(TransactionLegacyUnsigned {
536 chain_id: Some(U256::from(1)),
537 to: Some(to),
538 gas: U256::from(21_000),
539 ..Default::default()
540 })
541 }
542
543 fn gas_info() -> ReceiptGasInfo {
544 ReceiptGasInfo {
545 gas_used: U256::from(21_000),
546 effective_gas_price: U256::from(1_000_000_000),
547 }
548 }
549
550 #[test]
551 fn build_receipt_for_call() {
552 let extractor = ReceiptExtractor::new_mock();
553 let account = Account::default();
554 let eth_block_hash = H256::from([0xAB; 32]);
555 let block_number = U256::from(42);
556 let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
557
558 let (signed_tx, receipt) = extractor
560 .decode_transaction_and_build_receipt(
561 eth_block_hash,
562 block_number,
563 call,
564 tx_hash,
565 3,
566 gas_info(),
567 false,
568 vec![],
569 )
570 .unwrap();
571
572 assert!(receipt.is_success());
573 assert_eq!(receipt.from, account.address());
574 assert_eq!(receipt.to, Some(account.address()));
575 assert_eq!(receipt.contract_address, None);
576 assert_eq!(receipt.block_hash, eth_block_hash);
577 assert_eq!(receipt.block_number, block_number);
578 assert_eq!(receipt.transaction_hash, tx_hash);
579 assert_eq!(receipt.transaction_index, U256::from(3));
580 assert_eq!(receipt.gas_used, U256::from(21_000));
581 assert_eq!(signed_tx.recover_eth_address().unwrap(), account.address());
582
583 let (call, tx_hash) = signed_call(&account, legacy_call_tx(account.address()));
585 let (_, receipt) = extractor
586 .decode_transaction_and_build_receipt(
587 eth_block_hash,
588 block_number,
589 call,
590 tx_hash,
591 3,
592 gas_info(),
593 true,
594 vec![],
595 )
596 .unwrap();
597
598 assert!(!receipt.is_success());
599 assert_eq!(receipt.from, account.address());
600 }
601
602 #[test]
603 fn build_receipt_for_deploy() {
604 let extractor = ReceiptExtractor::new_mock();
605 let account = Account::default();
606 let deploy_tx = TransactionUnsigned::from(TransactionLegacyUnsigned {
607 chain_id: Some(U256::from(1)),
608 gas: U256::from(100_000),
609 nonce: U256::from(0),
610 ..Default::default()
611 });
612 let (call, tx_hash) = signed_call(&account, deploy_tx);
613
614 let (_, receipt) = extractor
615 .decode_transaction_and_build_receipt(
616 H256::zero(),
617 U256::from(1),
618 call,
619 tx_hash,
620 0,
621 gas_info(),
622 false,
623 vec![],
624 )
625 .unwrap();
626
627 assert!(receipt.is_success());
628 assert_eq!(receipt.to, None);
629 assert_eq!(receipt.contract_address, Some(create1(&account.address(), 0)));
630 assert_eq!(receipt.from, account.address());
631 }
632
633 #[test]
634 fn build_receipt_rejects_invalid_payload() {
635 let extractor = ReceiptExtractor::new_mock();
636
637 let call = EthTransact { payload: vec![0xde, 0xad] };
639 let hash = H256(keccak_256(&call.payload));
640 let err = extractor
641 .decode_transaction_and_build_receipt(
642 H256::zero(),
643 U256::from(1),
644 call,
645 hash,
646 0,
647 gas_info(),
648 false,
649 vec![],
650 )
651 .unwrap_err();
652 assert!(matches!(err, ClientError::TxDecodingFailed));
653
654 let extractor = ReceiptExtractor {
656 recover_eth_address: Arc::new(|_| Err(())),
657 ..ReceiptExtractor::new_mock()
658 };
659 let account = Account::default();
660 let (call, hash) = signed_call(&account, legacy_call_tx(account.address()));
661 let err = extractor
662 .decode_transaction_and_build_receipt(
663 H256::zero(),
664 U256::from(1),
665 call,
666 hash,
667 0,
668 gas_info(),
669 false,
670 vec![],
671 )
672 .unwrap_err();
673 assert!(matches!(err, ClientError::RecoverEthAddressFailed));
674 }
675
676 #[test]
677 fn defaults_and_first_evm_block_only_decreases() {
678 let extractor = ReceiptExtractor::new_mock();
679
680 assert!(extractor.first_evm_block().is_none());
681
682 extractor.set_first_evm_block(100);
684 assert_eq!(extractor.first_evm_block(), Some(100));
685
686 extractor.set_first_evm_block(50);
687 assert_eq!(extractor.first_evm_block(), Some(50));
688
689 extractor.set_first_evm_block(100);
691 assert_eq!(extractor.first_evm_block(), Some(50));
692 }
693
694 use codec::{Compact, Decode, Encode};
695 use frame_system::EventRecord;
696 use revive_dev_runtime::{Runtime, RuntimeEvent};
697 use subxt::{events::Events, metadata::Metadata};
698
699 struct EventsBuilder {
701 metadata: Metadata,
702 bytes: Vec<u8>,
703 count: u32,
704 }
705
706 impl EventsBuilder {
707 fn new() -> Self {
708 let metadata_bytes: &[u8] =
709 include_bytes!(concat!(env!("OUT_DIR"), "/revive_chain.scale"));
710 let metadata = Metadata::decode(&mut &metadata_bytes[..]).unwrap();
711 Self { metadata, bytes: Vec::new(), count: 0 }
712 }
713
714 fn push_event(
715 mut self,
716 phase: frame_system::Phase,
717 event: pallet_revive::Event<Runtime>,
718 ) -> Self {
719 EventRecord::<RuntimeEvent, H256> {
720 phase,
721 event: RuntimeEvent::Revive(event),
722 topics: vec![],
723 }
724 .encode_to(&mut self.bytes);
725 self.count += 1;
726 self
727 }
728
729 fn build(self) -> Events<SrcChainConfig> {
730 let mut encoded_events = Vec::new();
731 Compact(self.count).encode_to(&mut encoded_events);
732 encoded_events.extend(self.bytes);
733 Events::decode_from(encoded_events, self.metadata)
734 }
735 }
736
737 #[test]
738 fn extract_revive_events_decodes_contract_emitted_log() {
739 let contract = H160::from([0x11; 20]);
740 let topics = vec![H256::from([0x22; 32]), H256::from([0x33; 32])];
741 let data = vec![0xde, 0xad, 0xbe, 0xef];
742 let events = EventsBuilder::new()
743 .push_event(
744 frame_system::Phase::ApplyExtrinsic(5),
745 pallet_revive::Event::ContractEmitted {
746 contract,
747 data: data.clone(),
748 topics: topics.clone(),
749 },
750 )
751 .build();
752
753 let tx_hash = H256::from([0xAA; 32]);
754 let eth_block_hash = H256::from([0xBB; 32]);
755 let substrate_block_number = 42u32;
756 let eth_block_number = U256::from(substrate_block_number);
757
758 let (reverts, logs) = extract_revive_events(
759 &events,
760 substrate_block_number,
761 eth_block_number,
762 eth_block_hash,
763 |idx| (idx == 5).then_some(tx_hash),
764 );
765
766 assert!(reverts.is_empty());
767 assert_eq!(logs.len(), 1);
768 let log = &logs[&5][0];
769 assert_eq!(log.address, contract);
770 assert_eq!(log.topics, topics);
771 assert_eq!(log.data.as_ref().unwrap().0, data);
772 assert_eq!(log.block_hash, eth_block_hash);
773 assert_eq!(log.block_number, eth_block_number);
774 assert_eq!(log.transaction_hash, tx_hash);
775 assert_eq!(log.transaction_index, U256::from(5));
776 }
777
778 #[test]
779 fn extract_revive_events_skips_irrelevant_events() {
780 let empty_contract_emitted = pallet_revive::Event::ContractEmitted {
783 contract: H160::zero(),
784 data: vec![],
785 topics: vec![],
786 };
787 let revert = pallet_revive::Event::EthExtrinsicRevert {
788 dispatch_error: sp_runtime::DispatchError::Other("skipped-phase revert"),
789 };
790 let events = EventsBuilder::new()
791 .push_event(frame_system::Phase::Finalization, empty_contract_emitted.clone())
792 .push_event(frame_system::Phase::Initialization, revert.clone())
793 .push_event(frame_system::Phase::ApplyExtrinsic(5), empty_contract_emitted)
794 .push_event(frame_system::Phase::ApplyExtrinsic(5), revert)
795 .build();
796
797 let (reverts, logs) =
799 extract_revive_events(&events, 0, U256::zero(), H256::zero(), |idx| {
800 (idx == 7).then_some(H256::zero())
801 });
802
803 assert!(reverts.is_empty());
804 assert!(logs.is_empty());
805 }
806
807 #[test]
808 fn extract_revive_events_accumulates_per_extrinsic() {
809 let tx0 = H256::from([0x01; 32]);
810 let tx1 = H256::from([0x02; 32]);
811 let tx2 = H256::from([0x03; 32]);
812 let emitted_by = |contract: H160| pallet_revive::Event::ContractEmitted {
813 contract,
814 data: vec![],
815 topics: vec![],
816 };
817 let events = EventsBuilder::new()
818 .push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xaa; 20])))
819 .push_event(frame_system::Phase::ApplyExtrinsic(0), emitted_by(H160::from([0xbb; 20])))
820 .push_event(
821 frame_system::Phase::ApplyExtrinsic(1),
822 pallet_revive::Event::EthExtrinsicRevert {
823 dispatch_error: sp_runtime::DispatchError::Other("tx-1 revert"),
824 },
825 )
826 .push_event(frame_system::Phase::ApplyExtrinsic(2), emitted_by(H160::from([0xcc; 20])))
827 .build();
828
829 let (reverts, logs) =
830 extract_revive_events(&events, 0, U256::zero(), H256::zero(), |idx| match idx {
831 0 => Some(tx0),
832 1 => Some(tx1),
833 2 => Some(tx2),
834 _ => None,
835 });
836
837 assert_eq!(reverts, [1usize].into_iter().collect::<HashSet<_>>());
838 assert_eq!(logs[&0].len(), 2);
839 assert_eq!(logs[&2].len(), 1);
840 assert_eq!(logs[&0][0].log_index, U256::from(0));
842 assert_eq!(logs[&0][1].log_index, U256::from(1));
843 assert_eq!(logs[&2][0].log_index, U256::from(3));
844 }
845}