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