Files
pezkuwi-sdk/bizinikiwi/pezframe/revive/rpc/src/receipt_extractor.rs
T
pezkuwichain 65b7f5e640 fix: Resolve cargo clippy errors and add CI workflow plan
## Changes

### Clippy Fixes
- Fixed deprecated `cargo_bin` usage in 27 test files (added #![allow(deprecated)])
- Fixed uninlined_format_args in zombienet-sdk-tests
- Fixed subxt API changes in revive/rpc/tests.rs (fetch signature, StorageValue)
- Fixed dead_code warnings in validator-pool and identity-kyc mocks
- Fixed field name `i` -> `_i` in tasks example

### CI Infrastructure
- Added .claude/WORKFLOW_PLAN.md for tracking CI fix progress
- Updated lychee.toml and taplo.toml configs

### Files Modified
- 27 test files with deprecated cargo_bin fix
- bizinikiwi/pezframe/revive/rpc/src/tests.rs (subxt API)
- pezkuwi/pezpallets/validator-pool/src/{mock,tests}.rs
- pezcumulus/teyrchains/pezpallets/identity-kyc/src/mock.rs
- bizinikiwi/pezframe/examples/tasks/src/tests.rs

## Status
- cargo clippy: PASSING
- Next: cargo fmt, zepter, workspace checks
2025-12-23 09:37:11 +03:00

359 lines
11 KiB
Rust

// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::{
client::{runtime_api::RuntimeApi, BizinikiwiBlock, BizinikiwiBlockNumber},
subxt_client::{
revive::{
calls::types::EthTransact,
events::{ContractEmitted, EthExtrinsicRevert},
},
SrcChainConfig,
},
ClientError, H160, LOG_TARGET,
};
use futures::{stream, StreamExt};
use pezkuwi_subxt::{blocks::ExtrinsicDetails, OnlineClient};
use pezpallet_revive::{
create1,
evm::{GenericTransaction, Log, ReceiptGasInfo, ReceiptInfo, TransactionSigned, H256, U256},
};
use pezsp_core::keccak_256;
use std::{future::Future, pin::Pin, sync::Arc};
type FetchReceiptDataFn = Arc<
dyn Fn(H256) -> Pin<Box<dyn Future<Output = Option<Vec<ReceiptGasInfo>>> + Send>> + Send + Sync,
>;
type FetchEthBlockHashFn =
Arc<dyn Fn(H256, u64) -> Pin<Box<dyn Future<Output = Option<H256>> + Send>> + Send + Sync>;
type RecoverEthAddressFn = Arc<dyn Fn(&TransactionSigned) -> Result<H160, ()> + Send + Sync>;
/// Utility to extract receipts from extrinsics.
#[derive(Clone)]
pub struct ReceiptExtractor {
/// Fetch the receipt data info.
fetch_receipt_data: FetchReceiptDataFn,
/// Fetch ethereum block hash.
fetch_eth_block_hash: FetchEthBlockHashFn,
/// Earliest block number to consider when searching for transaction receipts.
earliest_receipt_block: Option<BizinikiwiBlockNumber>,
/// Recover the ethereum address from a transaction signature.
recover_eth_address: RecoverEthAddressFn,
}
impl ReceiptExtractor {
/// Check if the block is before the earliest block.
pub fn is_before_earliest_block(&self, block_number: BizinikiwiBlockNumber) -> bool {
block_number < self.earliest_receipt_block.unwrap_or_default()
}
/// Create a new `ReceiptExtractor` with the given native to eth ratio.
pub async fn new(
api: OnlineClient<SrcChainConfig>,
earliest_receipt_block: Option<BizinikiwiBlockNumber>,
) -> Result<Self, ClientError> {
Self::new_with_custom_address_recovery(
api,
earliest_receipt_block,
Arc::new(|signed_tx: &TransactionSigned| signed_tx.recover_eth_address()),
)
.await
}
/// Create a new `ReceiptExtractor` with the given native to eth ratio.
///
/// Specify also a custom Ethereum address recovery logic.
/// Use `ReceiptExtractor::new` if the default Ethereum address recovery
/// logic ([`TransactionSigned::recover_eth_address`] based) is enough.
pub async fn new_with_custom_address_recovery(
api: OnlineClient<SrcChainConfig>,
earliest_receipt_block: Option<BizinikiwiBlockNumber>,
recover_eth_address_fn: RecoverEthAddressFn,
) -> Result<Self, ClientError> {
let api_inner = api.clone();
let fetch_eth_block_hash = Arc::new(move |block_hash, block_number| {
let api_inner = api_inner.clone();
let fut = async move {
let runtime_api = RuntimeApi::new(api_inner.runtime_api().at(block_hash));
runtime_api.eth_block_hash(U256::from(block_number)).await.ok().flatten()
};
Box::pin(fut) as Pin<Box<_>>
});
let api_inner = api.clone();
let fetch_receipt_data = Arc::new(move |block_hash| {
let api_inner = api_inner.clone();
let fut = async move {
let runtime_api = RuntimeApi::new(api_inner.runtime_api().at(block_hash));
runtime_api.eth_receipt_data().await.ok()
};
Box::pin(fut) as Pin<Box<_>>
});
Ok(Self {
fetch_receipt_data,
fetch_eth_block_hash,
earliest_receipt_block,
recover_eth_address: recover_eth_address_fn,
})
}
#[cfg(test)]
pub fn new_mock() -> Self {
let fetch_receipt_data = Arc::new(|_| Box::pin(std::future::ready(None)) as Pin<Box<_>>);
// This method is useful when testing eth - bizinikiwi mapping.
let fetch_eth_block_hash = Arc::new(|block_hash: H256, block_number: u64| {
// Generate hash from bizinikiwi block hash and number
let bytes: Vec<u8> = [block_hash.as_bytes(), &block_number.to_be_bytes()].concat();
let eth_block_hash = H256::from(keccak_256(&bytes));
Box::pin(std::future::ready(Some(eth_block_hash))) as Pin<Box<_>>
});
Self {
fetch_receipt_data,
fetch_eth_block_hash,
earliest_receipt_block: None,
recover_eth_address: Arc::new(|signed_tx: &TransactionSigned| {
signed_tx.recover_eth_address()
}),
}
}
/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] from an extrinsic.
async fn extract_from_extrinsic(
&self,
bizinikiwi_block: &BizinikiwiBlock,
eth_block_hash: H256,
ext: pezkuwi_subxt::blocks::ExtrinsicDetails<
SrcChainConfig,
pezkuwi_subxt::OnlineClient<SrcChainConfig>,
>,
call: EthTransact,
receipt_gas_info: ReceiptGasInfo,
transaction_index: usize,
) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
let events = ext.events().await?;
let block_number: U256 = bizinikiwi_block.number().into();
let success = !events.has::<EthExtrinsicRevert>().inspect_err(|err| {
log::debug!(
target: LOG_TARGET,
"Failed to lookup for EthExtrinsicRevert event in block {block_number}: {err:?}"
);
})?;
let transaction_hash = H256(keccak_256(&call.payload));
let signed_tx =
TransactionSigned::decode(&call.payload).map_err(|_| ClientError::TxDecodingFailed)?;
let from = (self.recover_eth_address)(&signed_tx).map_err(|_| {
log::error!(target: LOG_TARGET, "Failed to recover eth address from signed tx");
ClientError::RecoverEthAddressFailed
})?;
let tx_info = GenericTransaction::from_signed(
signed_tx.clone(),
receipt_gas_info.effective_gas_price,
Some(from),
);
// get logs from ContractEmitted event
let logs = events
.iter()
.filter_map(|event_details| {
let event_details = event_details.ok()?;
let event = event_details.as_event::<ContractEmitted>().ok()??;
Some(Log {
address: event.contract,
topics: event.topics,
data: Some(event.data.into()),
block_number,
transaction_hash,
transaction_index: transaction_index.into(),
block_hash: eth_block_hash,
log_index: event_details.index().into(),
..Default::default()
})
})
.collect();
let contract_address = if tx_info.to.is_none() {
Some(create1(
&from,
tx_info
.nonce
.unwrap_or_default()
.try_into()
.map_err(|_| ClientError::ConversionFailed)?,
))
} else {
None
};
let receipt = ReceiptInfo::new(
eth_block_hash,
block_number,
contract_address,
from,
logs,
tx_info.to,
receipt_gas_info.effective_gas_price,
U256::from(receipt_gas_info.gas_used),
success,
transaction_hash,
transaction_index.into(),
tx_info.r#type.unwrap_or_default(),
);
Ok((signed_tx, receipt))
}
/// Extract receipts from block.
pub async fn extract_from_block(
&self,
block: &BizinikiwiBlock,
) -> Result<Vec<(TransactionSigned, ReceiptInfo)>, ClientError> {
if self.is_before_earliest_block(block.number()) {
return Ok(vec![]);
}
let ext_iter = self.get_block_extrinsics(block).await?;
let bizinikiwi_block_number = block.number() as u64;
let bizinikiwi_block_hash = block.hash();
let eth_block_hash =
(self.fetch_eth_block_hash)(bizinikiwi_block_hash, bizinikiwi_block_number)
.await
.unwrap_or(bizinikiwi_block_hash);
// Process extrinsics in order while maintaining parallelism within buffer window
stream::iter(ext_iter)
.map(|(ext, call, receipt, ext_idx)| async move {
self.extract_from_extrinsic(block, eth_block_hash, ext, call, receipt, ext_idx)
.await
.inspect_err(|err| {
log::warn!(target: LOG_TARGET, "Error extracting extrinsic: {err:?}");
})
})
.buffered(10)
.collect::<Vec<Result<_, _>>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
}
/// Return the ETH extrinsics of the block grouped with reconstruction receipt info and
/// extrinsic index
pub async fn get_block_extrinsics(
&self,
block: &BizinikiwiBlock,
) -> Result<
impl Iterator<
Item = (
ExtrinsicDetails<SrcChainConfig, OnlineClient<SrcChainConfig>>,
EthTransact,
ReceiptGasInfo,
usize,
),
>,
ClientError,
> {
// Filter extrinsics from pezpallet_revive
let extrinsics = block.extrinsics().await.inspect_err(|err| {
log::debug!(target: LOG_TARGET, "Error fetching for #{:?} extrinsics: {err:?}", block.number());
})?;
let receipt_data = (self.fetch_receipt_data)(block.hash())
.await
.ok_or(ClientError::ReceiptDataNotFound)?;
let extrinsics: Vec<_> = extrinsics
.iter()
.enumerate()
.flat_map(|(ext_idx, ext)| {
let call = ext.as_extrinsic::<EthTransact>().ok()??;
Some((ext, call, ext_idx))
})
.collect();
// Sanity check we received enough data from the pezpallet revive.
if receipt_data.len() != extrinsics.len() {
log::error!(
target: LOG_TARGET,
"Receipt data length ({}) does not match extrinsics length ({})",
receipt_data.len(),
extrinsics.len()
);
Err(ClientError::ReceiptDataLengthMismatch)
} else {
Ok(extrinsics
.into_iter()
.zip(receipt_data)
.map(|((extr, call, ext_idx), rec)| (extr, call, rec, ext_idx)))
}
}
/// Extract a [`TransactionSigned`] and a [`ReceiptInfo`] for a specific transaction in a
/// [`BizinikiwiBlock`]
pub async fn extract_from_transaction(
&self,
block: &BizinikiwiBlock,
transaction_index: usize,
) -> Result<(TransactionSigned, ReceiptInfo), ClientError> {
let ext_iter = self.get_block_extrinsics(block).await?;
let (ext, eth_call, receipt_gas_info, _) = ext_iter
.into_iter()
.find(|(_, _, _, ext_idx)| *ext_idx == transaction_index)
.ok_or(ClientError::EthExtrinsicNotFound)?;
let bizinikiwi_block_number = block.number() as u64;
let bizinikiwi_block_hash = block.hash();
let eth_block_hash =
(self.fetch_eth_block_hash)(bizinikiwi_block_hash, bizinikiwi_block_number)
.await
.unwrap_or(bizinikiwi_block_hash);
self.extract_from_extrinsic(
block,
eth_block_hash,
ext,
eth_call,
receipt_gas_info,
transaction_index,
)
.await
}
/// Get the Ethereum block hash for the Bizinikiwi block with specific hash.
pub async fn get_ethereum_block_hash(
&self,
block_hash: &H256,
block_number: u64,
) -> Option<H256> {
(self.fetch_eth_block_hash)(*block_hash, block_number).await
}
}