b6d35f6faf
Updated 4763 files with dual copyright: - Parity Technologies (UK) Ltd. - Dijital Kurdistan Tech Institute
843 lines
27 KiB
Rust
843 lines
27 KiB
Rust
// This file is part of Bizinikiwi.
|
|
|
|
// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
|
|
// 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.
|
|
//! The client connects to the source bizinikiwi chain
|
|
//! and is used by the rpc server to query and send transactions to the bizinikiwi chain.
|
|
|
|
pub(crate) mod runtime_api;
|
|
pub(crate) mod storage_api;
|
|
|
|
use crate::{
|
|
subxt_client::{self, revive::calls::types::EthTransact, SrcChainConfig},
|
|
BlockInfoProvider, BlockTag, FeeHistoryProvider, ReceiptProvider, SubxtBlockInfoProvider,
|
|
TracerType, TransactionInfo,
|
|
};
|
|
use jsonrpsee::types::{error::CALL_EXECUTION_FAILED_CODE, ErrorObjectOwned};
|
|
use pezkuwi_subxt::{
|
|
backend::{
|
|
legacy::{rpc_methods::SystemHealth, LegacyRpcMethods},
|
|
rpc::{
|
|
reconnecting_rpc_client::{ExponentialBackoff, RpcClient as ReconnectingRpcClient},
|
|
RpcClient,
|
|
},
|
|
},
|
|
config::{HashFor, Header},
|
|
ext::pezkuwi_subxt_rpcs::rpc_params,
|
|
Config, OnlineClient,
|
|
};
|
|
use pezpallet_revive::{
|
|
evm::{
|
|
decode_revert_reason, Block, BlockNumberOrTag, BlockNumberOrTagOrHash, FeeHistoryResult,
|
|
Filter, GenericTransaction, HashesOrTransactionInfos, Log, ReceiptInfo, SyncingProgress,
|
|
SyncingStatus, Trace, TransactionSigned, TransactionTrace, H256,
|
|
},
|
|
EthTransactError,
|
|
};
|
|
use pezsp_runtime::traits::Block as BlockT;
|
|
use pezsp_weights::Weight;
|
|
use runtime_api::RuntimeApi;
|
|
use std::{ops::Range, sync::Arc, time::Duration};
|
|
use storage_api::StorageApi;
|
|
use thiserror::Error;
|
|
use tokio::sync::Mutex;
|
|
|
|
/// The bizinikiwi block type.
|
|
pub type BizinikiwiBlock =
|
|
pezkuwi_subxt::blocks::Block<SrcChainConfig, OnlineClient<SrcChainConfig>>;
|
|
|
|
/// The bizinikiwi block header.
|
|
pub type BizinikiwiBlockHeader = <SrcChainConfig as Config>::Header;
|
|
|
|
/// The bizinikiwi block number type.
|
|
pub type BizinikiwiBlockNumber = <BizinikiwiBlockHeader as Header>::Number;
|
|
|
|
/// The bizinikiwi block hash type.
|
|
pub type BizinikiwiBlockHash = HashFor<SrcChainConfig>;
|
|
|
|
/// The runtime balance type.
|
|
pub type Balance = u128;
|
|
|
|
/// The subscription type used to listen to new blocks.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum SubscriptionType {
|
|
/// Subscribe to best blocks.
|
|
BestBlocks,
|
|
/// Subscribe to finalized blocks.
|
|
FinalizedBlocks,
|
|
}
|
|
|
|
/// The error type for the client.
|
|
#[derive(Error, Debug)]
|
|
pub enum ClientError {
|
|
/// A [`jsonrpsee::core::ClientError`] wrapper error.
|
|
#[error(transparent)]
|
|
Jsonrpsee(#[from] jsonrpsee::core::ClientError),
|
|
/// A [`pezkuwi_subxt::Error`] wrapper error.
|
|
#[error(transparent)]
|
|
SubxtError(#[from] pezkuwi_subxt::Error),
|
|
#[error(transparent)]
|
|
RpcError(#[from] pezkuwi_subxt::ext::pezkuwi_subxt_rpcs::Error),
|
|
/// A [`sqlx::Error`] wrapper error.
|
|
#[error(transparent)]
|
|
SqlxError(#[from] sqlx::Error),
|
|
/// A [`codec::Error`] wrapper error.
|
|
#[error(transparent)]
|
|
CodecError(#[from] codec::Error),
|
|
/// Transcact call failed.
|
|
#[error("contract reverted: {0:?}")]
|
|
TransactError(EthTransactError),
|
|
/// A decimal conversion failed.
|
|
#[error("conversion failed")]
|
|
ConversionFailed,
|
|
/// The block hash was not found.
|
|
#[error("hash not found")]
|
|
BlockNotFound,
|
|
/// The contract was not found.
|
|
#[error("Contract not found")]
|
|
ContractNotFound,
|
|
#[error("No Ethereum extrinsic found")]
|
|
EthExtrinsicNotFound,
|
|
/// The transaction fee could not be found
|
|
#[error("transactionFeePaid event not found")]
|
|
TxFeeNotFound,
|
|
/// Failed to decode a raw payload into a signed transaction.
|
|
#[error("Failed to decode a raw payload into a signed transaction")]
|
|
TxDecodingFailed,
|
|
/// Failed to recover eth address.
|
|
#[error("failed to recover eth address")]
|
|
RecoverEthAddressFailed,
|
|
/// Failed to filter logs.
|
|
#[error("Failed to filter logs")]
|
|
LogFilterFailed(#[from] anyhow::Error),
|
|
/// Receipt storage was not found.
|
|
#[error("Receipt storage not found")]
|
|
ReceiptDataNotFound,
|
|
/// Ethereum block was not found.
|
|
#[error("Ethereum block not found")]
|
|
EthereumBlockNotFound,
|
|
/// Receipt data length mismatch.
|
|
#[error("Receipt data length mismatch")]
|
|
ReceiptDataLengthMismatch,
|
|
}
|
|
|
|
impl From<pezkuwi_subxt::error::EventsError> for ClientError {
|
|
fn from(err: pezkuwi_subxt::error::EventsError) -> Self {
|
|
ClientError::SubxtError(pezkuwi_subxt::Error::from(err))
|
|
}
|
|
}
|
|
|
|
impl From<pezkuwi_subxt::error::ExtrinsicError> for ClientError {
|
|
fn from(err: pezkuwi_subxt::error::ExtrinsicError) -> Self {
|
|
ClientError::SubxtError(pezkuwi_subxt::Error::from(err))
|
|
}
|
|
}
|
|
|
|
impl From<pezkuwi_subxt::error::BlockError> for ClientError {
|
|
fn from(err: pezkuwi_subxt::error::BlockError) -> Self {
|
|
ClientError::SubxtError(pezkuwi_subxt::Error::from(err))
|
|
}
|
|
}
|
|
|
|
impl From<pezkuwi_subxt::error::BackendError> for ClientError {
|
|
fn from(err: pezkuwi_subxt::error::BackendError) -> Self {
|
|
ClientError::SubxtError(pezkuwi_subxt::Error::from(err))
|
|
}
|
|
}
|
|
|
|
impl From<pezkuwi_subxt::error::RuntimeApiError> for ClientError {
|
|
fn from(err: pezkuwi_subxt::error::RuntimeApiError) -> Self {
|
|
ClientError::SubxtError(pezkuwi_subxt::Error::from(err))
|
|
}
|
|
}
|
|
|
|
impl From<pezkuwi_subxt::error::ConstantError> for ClientError {
|
|
fn from(err: pezkuwi_subxt::error::ConstantError) -> Self {
|
|
ClientError::SubxtError(pezkuwi_subxt::Error::from(err))
|
|
}
|
|
}
|
|
|
|
impl From<pezkuwi_subxt::error::OnlineClientError> for ClientError {
|
|
fn from(err: pezkuwi_subxt::error::OnlineClientError) -> Self {
|
|
ClientError::SubxtError(pezkuwi_subxt::Error::from(err))
|
|
}
|
|
}
|
|
|
|
const LOG_TARGET: &str = "eth-rpc::client";
|
|
|
|
const REVERT_CODE: i32 = 3;
|
|
|
|
const NOTIFIER_CAPACITY: usize = 16;
|
|
impl From<ClientError> for ErrorObjectOwned {
|
|
fn from(err: ClientError) -> Self {
|
|
match err {
|
|
ClientError::SubxtError(pezkuwi_subxt::Error::OtherRpcClientError(
|
|
pezkuwi_subxt::ext::pezkuwi_subxt_rpcs::Error::User(err),
|
|
))
|
|
| ClientError::RpcError(pezkuwi_subxt::ext::pezkuwi_subxt_rpcs::Error::User(err)) => {
|
|
ErrorObjectOwned::owned::<Vec<u8>>(err.code, err.message, None)
|
|
},
|
|
ClientError::TransactError(EthTransactError::Data(data)) => {
|
|
let msg = match decode_revert_reason(&data) {
|
|
Some(reason) => format!("execution reverted: {reason}"),
|
|
None => "execution reverted".to_string(),
|
|
};
|
|
|
|
let data = format!("0x{}", hex::encode(data));
|
|
ErrorObjectOwned::owned::<String>(REVERT_CODE, msg, Some(data))
|
|
},
|
|
ClientError::TransactError(EthTransactError::Message(msg)) => {
|
|
ErrorObjectOwned::owned::<String>(CALL_EXECUTION_FAILED_CODE, msg, None)
|
|
},
|
|
_ => {
|
|
ErrorObjectOwned::owned::<String>(CALL_EXECUTION_FAILED_CODE, err.to_string(), None)
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A client connect to a node and maintains a cache of the last `CACHE_SIZE` blocks.
|
|
#[derive(Clone)]
|
|
pub struct Client {
|
|
api: OnlineClient<SrcChainConfig>,
|
|
rpc_client: RpcClient,
|
|
rpc: LegacyRpcMethods<SrcChainConfig>,
|
|
receipt_provider: ReceiptProvider,
|
|
block_provider: SubxtBlockInfoProvider,
|
|
fee_history_provider: FeeHistoryProvider,
|
|
chain_id: u64,
|
|
max_block_weight: Weight,
|
|
/// Whether the node has automine enabled.
|
|
automine: bool,
|
|
/// A notifier, that informs subscribers of new best blocks.
|
|
block_notifier: Option<tokio::sync::broadcast::Sender<H256>>,
|
|
/// A lock to ensure only one subscription can perform write operations at a time.
|
|
subscription_lock: Arc<Mutex<()>>,
|
|
}
|
|
|
|
/// Fetch the chain ID from the bizinikiwi chain.
|
|
async fn chain_id(api: &OnlineClient<SrcChainConfig>) -> Result<u64, ClientError> {
|
|
let query = subxt_client::constants().revive().chain_id();
|
|
api.constants().at(&query).map_err(|err| err.into())
|
|
}
|
|
|
|
/// Fetch the max block weight from the bizinikiwi chain.
|
|
async fn max_block_weight(api: &OnlineClient<SrcChainConfig>) -> Result<Weight, ClientError> {
|
|
let query = subxt_client::constants().system().block_weights();
|
|
let weights = api.constants().at(&query)?;
|
|
let max_block = weights.per_class.normal.max_extrinsic.unwrap_or(weights.max_block);
|
|
Ok(max_block.0)
|
|
}
|
|
|
|
/// Get the automine status from the node.
|
|
async fn get_automine(rpc_client: &RpcClient) -> bool {
|
|
match rpc_client.request::<bool>("getAutomine", rpc_params![]).await {
|
|
Ok(val) => val,
|
|
Err(err) => {
|
|
log::info!(target: LOG_TARGET, "Node does not have getAutomine RPC. Defaulting to automine=false. error: {err:?}");
|
|
false
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Connect to a node at the given URL, and return the underlying API, RPC client, and legacy RPC
|
|
/// clients.
|
|
pub async fn connect(
|
|
pez_node_rpc_url: &str,
|
|
) -> Result<(OnlineClient<SrcChainConfig>, RpcClient, LegacyRpcMethods<SrcChainConfig>), ClientError>
|
|
{
|
|
log::info!(target: LOG_TARGET, "🌐 Connecting to node at: {pez_node_rpc_url} ...");
|
|
let rpc_client = ReconnectingRpcClient::builder()
|
|
.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
|
|
.build(pez_node_rpc_url.to_string())
|
|
.await?;
|
|
let rpc_client = RpcClient::new(rpc_client);
|
|
log::info!(target: LOG_TARGET, "🌟 Connected to node at: {pez_node_rpc_url}");
|
|
|
|
let api = OnlineClient::<SrcChainConfig>::from_rpc_client(rpc_client.clone()).await?;
|
|
let rpc = LegacyRpcMethods::<SrcChainConfig>::new(rpc_client.clone());
|
|
Ok((api, rpc_client, rpc))
|
|
}
|
|
|
|
impl Client {
|
|
/// Create a new client instance.
|
|
pub async fn new(
|
|
api: OnlineClient<SrcChainConfig>,
|
|
rpc_client: RpcClient,
|
|
rpc: LegacyRpcMethods<SrcChainConfig>,
|
|
block_provider: SubxtBlockInfoProvider,
|
|
receipt_provider: ReceiptProvider,
|
|
) -> Result<Self, ClientError> {
|
|
let (chain_id, max_block_weight, automine) =
|
|
tokio::try_join!(chain_id(&api), max_block_weight(&api), async {
|
|
Ok(get_automine(&rpc_client).await)
|
|
},)?;
|
|
|
|
let client = Self {
|
|
api,
|
|
rpc_client,
|
|
rpc,
|
|
receipt_provider,
|
|
block_provider,
|
|
fee_history_provider: FeeHistoryProvider::default(),
|
|
chain_id,
|
|
max_block_weight,
|
|
automine,
|
|
block_notifier: automine
|
|
.then(|| tokio::sync::broadcast::channel::<H256>(NOTIFIER_CAPACITY).0),
|
|
subscription_lock: Arc::new(Mutex::new(())),
|
|
};
|
|
|
|
Ok(client)
|
|
}
|
|
|
|
/// Creates a block notifier instance.
|
|
pub fn create_block_notifier(&mut self) {
|
|
self.block_notifier = Some(tokio::sync::broadcast::channel::<H256>(NOTIFIER_CAPACITY).0);
|
|
}
|
|
|
|
/// Subscribe to past blocks executing the callback for each block in `range`.
|
|
async fn subscribe_past_blocks<F, Fut>(
|
|
&self,
|
|
range: Range<BizinikiwiBlockNumber>,
|
|
callback: F,
|
|
) -> Result<(), ClientError>
|
|
where
|
|
F: Fn(Arc<BizinikiwiBlock>) -> Fut + Send + Sync,
|
|
Fut: std::future::Future<Output = Result<(), ClientError>> + Send,
|
|
{
|
|
let mut block = self
|
|
.block_provider
|
|
.block_by_number(range.end)
|
|
.await?
|
|
.ok_or(ClientError::BlockNotFound)?;
|
|
|
|
loop {
|
|
let block_number = block.number();
|
|
log::trace!(target: "eth-rpc::subscription", "Processing past block #{block_number}");
|
|
|
|
let parent_hash = block.header().parent_hash;
|
|
callback(block.clone()).await.inspect_err(|err| {
|
|
log::error!(target: "eth-rpc::subscription", "Failed to process past block #{block_number}: {err:?}");
|
|
})?;
|
|
|
|
if range.start < block_number {
|
|
block = self
|
|
.block_provider
|
|
.block_by_hash(&parent_hash)
|
|
.await?
|
|
.ok_or(ClientError::BlockNotFound)?;
|
|
} else {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Subscribe to new blocks, and execute the async closure for each block.
|
|
async fn subscribe_new_blocks<F, Fut>(
|
|
&self,
|
|
subscription_type: SubscriptionType,
|
|
callback: F,
|
|
) -> Result<(), ClientError>
|
|
where
|
|
F: Fn(BizinikiwiBlock) -> Fut + Send + Sync,
|
|
Fut: std::future::Future<Output = Result<(), ClientError>> + Send,
|
|
{
|
|
let mut block_stream = match subscription_type {
|
|
SubscriptionType::BestBlocks => self.api.blocks().subscribe_best().await,
|
|
SubscriptionType::FinalizedBlocks => self.api.blocks().subscribe_finalized().await,
|
|
}
|
|
.inspect_err(|err| {
|
|
log::error!(target: LOG_TARGET, "Failed to subscribe to blocks: {err:?}");
|
|
})?;
|
|
|
|
while let Some(block) = block_stream.next().await {
|
|
let block = match block {
|
|
Ok(block) => block,
|
|
Err(err) => {
|
|
if err.is_disconnected_will_reconnect() {
|
|
log::warn!(
|
|
target: LOG_TARGET,
|
|
"The RPC connection was lost and we may have missed a few blocks ({subscription_type:?}): {err:?}"
|
|
);
|
|
continue;
|
|
}
|
|
|
|
log::error!(target: LOG_TARGET, "Failed to fetch block ({subscription_type:?}): {err:?}");
|
|
return Err(err.into());
|
|
},
|
|
};
|
|
|
|
// Acquire lock to ensure only one subscription can perform write operations at a time
|
|
let _guard = self.subscription_lock.lock().await;
|
|
|
|
let block_number = block.number();
|
|
log::trace!(target: "eth-rpc::subscription", "⏳ Processing {subscription_type:?} block: {block_number}");
|
|
if let Err(err) = callback(block).await {
|
|
log::error!(target: LOG_TARGET, "Failed to process block {block_number}: {err:?}");
|
|
} else {
|
|
log::trace!(target: "eth-rpc::subscription", "✅ Processed {subscription_type:?} block: {block_number}");
|
|
}
|
|
}
|
|
|
|
log::info!(target: LOG_TARGET, "Block subscription ended");
|
|
Ok(())
|
|
}
|
|
|
|
/// Start the block subscription, and populate the block cache.
|
|
pub async fn subscribe_and_cache_new_blocks(
|
|
&self,
|
|
subscription_type: SubscriptionType,
|
|
) -> Result<(), ClientError> {
|
|
log::info!(target: LOG_TARGET, "🔌 Subscribing to new blocks ({subscription_type:?})");
|
|
self.subscribe_new_blocks(subscription_type, |block| async {
|
|
let hash = block.hash();
|
|
let evm_block = self.runtime_api(hash).eth_block().await?;
|
|
let (_, receipts): (Vec<_>, Vec<_>) = self
|
|
.receipt_provider
|
|
.insert_block_receipts(&block, &evm_block.hash)
|
|
.await?
|
|
.into_iter()
|
|
.unzip();
|
|
|
|
self.block_provider.update_latest(Arc::new(block), subscription_type).await;
|
|
self.fee_history_provider.update_fee_history(&evm_block, &receipts).await;
|
|
|
|
// Only broadcast for best blocks to avoid duplicate notifications.
|
|
match (subscription_type, &self.block_notifier) {
|
|
(SubscriptionType::BestBlocks, Some(sender)) if sender.receiver_count() > 0 => {
|
|
let _ = sender.send(hash);
|
|
},
|
|
_ => {},
|
|
}
|
|
Ok(())
|
|
})
|
|
.await
|
|
}
|
|
|
|
/// Cache old blocks up to the given block number.
|
|
pub async fn subscribe_and_cache_blocks(
|
|
&self,
|
|
index_last_n_blocks: BizinikiwiBlockNumber,
|
|
) -> Result<(), ClientError> {
|
|
let last = self.latest_block().await.number().saturating_sub(1);
|
|
let range = last.saturating_sub(index_last_n_blocks)..last;
|
|
log::info!(target: LOG_TARGET, "🗄️ Indexing past blocks in range {range:?}");
|
|
|
|
self.subscribe_past_blocks(range, |block| async move {
|
|
let ethereum_hash = self
|
|
.runtime_api(block.hash())
|
|
.eth_block_hash(pezpallet_revive::evm::U256::from(block.number()))
|
|
.await?
|
|
.ok_or(ClientError::EthereumBlockNotFound)?;
|
|
self.receipt_provider.insert_block_receipts(&block, ðereum_hash).await?;
|
|
Ok(())
|
|
})
|
|
.await?;
|
|
|
|
log::info!(target: LOG_TARGET, "🗄️ Finished indexing past blocks");
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the block hash for the given block number or tag.
|
|
pub async fn block_hash_for_tag(
|
|
&self,
|
|
at: BlockNumberOrTagOrHash,
|
|
) -> Result<BizinikiwiBlockHash, ClientError> {
|
|
match at {
|
|
BlockNumberOrTagOrHash::BlockHash(hash) => self
|
|
.resolve_bizinikiwi_hash(&hash)
|
|
.await
|
|
.ok_or(ClientError::EthereumBlockNotFound),
|
|
BlockNumberOrTagOrHash::BlockNumber(block_number) => {
|
|
let n: BizinikiwiBlockNumber =
|
|
(block_number).try_into().map_err(|_| ClientError::ConversionFailed)?;
|
|
let hash = self.get_block_hash(n).await?.ok_or(ClientError::BlockNotFound)?;
|
|
Ok(hash)
|
|
},
|
|
BlockNumberOrTagOrHash::BlockTag(BlockTag::Finalized | BlockTag::Safe) => {
|
|
let block = self.latest_finalized_block().await;
|
|
Ok(block.hash())
|
|
},
|
|
BlockNumberOrTagOrHash::BlockTag(_) => {
|
|
let block = self.latest_block().await;
|
|
Ok(block.hash())
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Get the storage API for the given block.
|
|
pub fn storage_api(&self, block_hash: H256) -> StorageApi {
|
|
StorageApi::new(self.api.storage().at(block_hash), block_hash)
|
|
}
|
|
|
|
/// Get the runtime API for the given block.
|
|
pub fn runtime_api(&self, block_hash: H256) -> RuntimeApi {
|
|
RuntimeApi::new(self.api.runtime_api().at(block_hash))
|
|
}
|
|
|
|
/// Get the latest finalized block.
|
|
pub async fn latest_finalized_block(&self) -> Arc<BizinikiwiBlock> {
|
|
self.block_provider.latest_finalized_block().await
|
|
}
|
|
|
|
/// Get the latest best block.
|
|
pub async fn latest_block(&self) -> Arc<BizinikiwiBlock> {
|
|
self.block_provider.latest_block().await
|
|
}
|
|
|
|
/// Expose the transaction API.
|
|
pub async fn submit(
|
|
&self,
|
|
call: pezkuwi_subxt::tx::DefaultPayload<EthTransact>,
|
|
) -> Result<H256, ClientError> {
|
|
let ext = self.api.tx().create_unsigned(&call).map_err(ClientError::from)?;
|
|
let hash: H256 = self
|
|
.rpc_client
|
|
.request("author_submitExtrinsic", rpc_params![to_hex(ext.encoded())])
|
|
.await?;
|
|
log::debug!(target: LOG_TARGET, "Submitted transaction with bizinikiwi hash: {hash:?}");
|
|
Ok(hash)
|
|
}
|
|
|
|
/// Get an EVM transaction receipt by hash.
|
|
pub async fn receipt(&self, tx_hash: &H256) -> Option<ReceiptInfo> {
|
|
self.receipt_provider.receipt_by_hash(tx_hash).await
|
|
}
|
|
|
|
pub async fn sync_state(
|
|
&self,
|
|
) -> Result<pezsc_rpc::system::SyncState<BizinikiwiBlockNumber>, ClientError> {
|
|
let client = self.rpc_client.clone();
|
|
let sync_state: pezsc_rpc::system::SyncState<BizinikiwiBlockNumber> =
|
|
client.request("system_syncState", Default::default()).await?;
|
|
Ok(sync_state)
|
|
}
|
|
|
|
/// Get the syncing status of the chain.
|
|
pub async fn syncing(&self) -> Result<SyncingStatus, ClientError> {
|
|
let health = self.rpc.system_health().await?;
|
|
|
|
let status = if health.is_syncing {
|
|
let sync_state = self.sync_state().await?;
|
|
SyncingProgress {
|
|
current_block: Some(sync_state.current_block.into()),
|
|
highest_block: Some(sync_state.highest_block.into()),
|
|
starting_block: Some(sync_state.starting_block.into()),
|
|
}
|
|
.into()
|
|
} else {
|
|
SyncingStatus::Bool(false)
|
|
};
|
|
|
|
Ok(status)
|
|
}
|
|
|
|
/// Get an EVM transaction receipt by hash.
|
|
pub async fn receipt_by_hash_and_index(
|
|
&self,
|
|
block_hash: &H256,
|
|
transaction_index: usize,
|
|
) -> Option<ReceiptInfo> {
|
|
self.receipt_provider
|
|
.receipt_by_block_hash_and_index(block_hash, transaction_index)
|
|
.await
|
|
}
|
|
|
|
pub async fn signed_tx_by_hash(&self, tx_hash: &H256) -> Option<TransactionSigned> {
|
|
self.receipt_provider.signed_tx_by_hash(tx_hash).await
|
|
}
|
|
|
|
/// Get receipts count per block.
|
|
pub async fn receipts_count_per_block(
|
|
&self,
|
|
block_hash: &BizinikiwiBlockHash,
|
|
) -> Option<usize> {
|
|
self.receipt_provider.receipts_count_per_block(block_hash).await
|
|
}
|
|
|
|
/// Get an EVM transaction receipt by specified Ethereum block hash.
|
|
pub async fn receipt_by_ethereum_hash_and_index(
|
|
&self,
|
|
ethereum_hash: &H256,
|
|
transaction_index: usize,
|
|
) -> Option<ReceiptInfo> {
|
|
// Fallback: use hash as Bizinikiwi hash if Ethereum hash cannot be resolved
|
|
let bizinikiwi_hash =
|
|
self.resolve_bizinikiwi_hash(ethereum_hash).await.unwrap_or(*ethereum_hash);
|
|
self.receipt_by_hash_and_index(&bizinikiwi_hash, transaction_index).await
|
|
}
|
|
|
|
/// Get the system health.
|
|
pub async fn system_health(&self) -> Result<SystemHealth, ClientError> {
|
|
let health = self.rpc.system_health().await?;
|
|
Ok(health)
|
|
}
|
|
|
|
/// Get the block number of the latest block.
|
|
pub async fn block_number(&self) -> Result<BizinikiwiBlockNumber, ClientError> {
|
|
let latest_block = self.block_provider.latest_block().await;
|
|
Ok(latest_block.number())
|
|
}
|
|
|
|
/// Get a block hash for the given block number.
|
|
pub async fn get_block_hash(
|
|
&self,
|
|
block_number: BizinikiwiBlockNumber,
|
|
) -> Result<Option<BizinikiwiBlockHash>, ClientError> {
|
|
let maybe_block = self.block_provider.block_by_number(block_number).await?;
|
|
Ok(maybe_block.map(|block| block.hash()))
|
|
}
|
|
|
|
/// Get a block for the specified hash or number.
|
|
pub async fn block_by_number_or_tag(
|
|
&self,
|
|
block: &BlockNumberOrTag,
|
|
) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError> {
|
|
match block {
|
|
BlockNumberOrTag::U256(n) => {
|
|
let n = (*n).try_into().map_err(|_| ClientError::ConversionFailed)?;
|
|
self.block_by_number(n).await
|
|
},
|
|
BlockNumberOrTag::BlockTag(BlockTag::Finalized | BlockTag::Safe) => {
|
|
let block = self.block_provider.latest_finalized_block().await;
|
|
Ok(Some(block))
|
|
},
|
|
BlockNumberOrTag::BlockTag(_) => {
|
|
let block = self.block_provider.latest_block().await;
|
|
Ok(Some(block))
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Get a block by hash
|
|
pub async fn block_by_hash(
|
|
&self,
|
|
hash: &BizinikiwiBlockHash,
|
|
) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError> {
|
|
self.block_provider.block_by_hash(hash).await
|
|
}
|
|
|
|
/// Resolve Ethereum block hash to Bizinikiwi block hash, then get the block.
|
|
/// This method provides the abstraction layer needed by the RPC APIs.
|
|
pub async fn resolve_bizinikiwi_hash(&self, ethereum_hash: &H256) -> Option<H256> {
|
|
self.receipt_provider.get_bizinikiwi_hash(ethereum_hash).await
|
|
}
|
|
|
|
/// Resolve Bizinikiwi block hash to Ethereum block hash, then get the block.
|
|
/// This method provides the abstraction layer needed by the RPC APIs.
|
|
pub async fn resolve_ethereum_hash(&self, bizinikiwi_hash: &H256) -> Option<H256> {
|
|
self.receipt_provider.get_ethereum_hash(bizinikiwi_hash).await
|
|
}
|
|
|
|
/// Get a block by Ethereum hash with automatic resolution to Bizinikiwi hash.
|
|
/// Falls back to treating the hash as a Bizinikiwi hash if no mapping exists.
|
|
pub async fn block_by_ethereum_hash(
|
|
&self,
|
|
ethereum_hash: &H256,
|
|
) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError> {
|
|
// First try to resolve the Ethereum hash to a Bizinikiwi hash
|
|
if let Some(bizinikiwi_hash) = self.resolve_bizinikiwi_hash(ethereum_hash).await {
|
|
return self.block_by_hash(&bizinikiwi_hash).await;
|
|
}
|
|
|
|
// Fallback: treat the provided hash as a Bizinikiwi hash (backward compatibility)
|
|
self.block_by_hash(ethereum_hash).await
|
|
}
|
|
|
|
/// Get a block by number
|
|
pub async fn block_by_number(
|
|
&self,
|
|
block_number: BizinikiwiBlockNumber,
|
|
) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError> {
|
|
self.block_provider.block_by_number(block_number).await
|
|
}
|
|
|
|
async fn tracing_block(
|
|
&self,
|
|
block_hash: H256,
|
|
) -> Result<
|
|
pezsp_runtime::generic::Block<
|
|
pezsp_runtime::generic::Header<u32, pezsp_runtime::traits::BlakeTwo256>,
|
|
pezsp_runtime::OpaqueExtrinsic,
|
|
>,
|
|
ClientError,
|
|
> {
|
|
let signed_block: pezsp_runtime::generic::SignedBlock<
|
|
pezsp_runtime::generic::Block<
|
|
pezsp_runtime::generic::Header<u32, pezsp_runtime::traits::BlakeTwo256>,
|
|
pezsp_runtime::OpaqueExtrinsic,
|
|
>,
|
|
> = self
|
|
.rpc_client
|
|
.request("chain_getBlock", rpc_params![block_hash])
|
|
.await
|
|
.unwrap();
|
|
|
|
Ok(signed_block.block)
|
|
}
|
|
|
|
/// Get the transaction traces for the given block.
|
|
pub async fn trace_block_by_number(
|
|
&self,
|
|
at: BlockNumberOrTag,
|
|
config: TracerType,
|
|
) -> Result<Vec<TransactionTrace>, ClientError> {
|
|
if self.receipt_provider.is_before_earliest_block(&at) {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
let block_hash = self.block_hash_for_tag(at.into()).await?;
|
|
let block = self.tracing_block(block_hash).await?;
|
|
let parent_hash = block.header().parent_hash;
|
|
let runtime_api = RuntimeApi::new(self.api.runtime_api().at(parent_hash));
|
|
let traces = runtime_api.trace_block(block, config.clone()).await?;
|
|
|
|
let mut hashes = self
|
|
.receipt_provider
|
|
.block_transaction_hashes(&block_hash)
|
|
.await
|
|
.ok_or(ClientError::EthExtrinsicNotFound)?;
|
|
|
|
let traces = traces.into_iter().filter_map(|(index, trace)| {
|
|
Some(TransactionTrace { tx_hash: hashes.remove(&(index as usize))?, trace })
|
|
});
|
|
|
|
Ok(traces.collect())
|
|
}
|
|
|
|
/// Get the transaction traces for the given transaction.
|
|
pub async fn trace_transaction(
|
|
&self,
|
|
transaction_hash: H256,
|
|
config: TracerType,
|
|
) -> Result<Trace, ClientError> {
|
|
let (block_hash, transaction_index) = self
|
|
.receipt_provider
|
|
.find_transaction(&transaction_hash)
|
|
.await
|
|
.ok_or(ClientError::EthExtrinsicNotFound)?;
|
|
|
|
let block = self.tracing_block(block_hash).await?;
|
|
let parent_hash = block.header.parent_hash;
|
|
let runtime_api = self.runtime_api(parent_hash);
|
|
|
|
runtime_api.trace_tx(block, transaction_index as u32, config).await
|
|
}
|
|
|
|
/// Get the transaction traces for the given block.
|
|
pub async fn trace_call(
|
|
&self,
|
|
transaction: GenericTransaction,
|
|
block: BlockNumberOrTagOrHash,
|
|
config: TracerType,
|
|
) -> Result<Trace, ClientError> {
|
|
let block_hash = self.block_hash_for_tag(block).await?;
|
|
let runtime_api = self.runtime_api(block_hash);
|
|
runtime_api.trace_call(transaction, config).await
|
|
}
|
|
|
|
/// Get the EVM block for the given Bizinikiwi block.
|
|
pub async fn evm_block(
|
|
&self,
|
|
block: Arc<BizinikiwiBlock>,
|
|
hydrated_transactions: bool,
|
|
) -> Option<Block> {
|
|
log::trace!(target: LOG_TARGET, "Get Ethereum block for hash {:?}", block.hash());
|
|
|
|
// This could potentially fail under below circumstances:
|
|
// - state has been pruned
|
|
// - the block author cannot be obtained from the digest logs (highly unlikely)
|
|
// - the node we are targeting has an outdated revive pezpallet (or ETH block functionality
|
|
// is disabled)
|
|
match self.runtime_api(block.hash()).eth_block().await {
|
|
Ok(mut eth_block) => {
|
|
log::trace!(target: LOG_TARGET, "Ethereum block from runtime API hash {:?}", eth_block.hash);
|
|
|
|
if hydrated_transactions {
|
|
// Hydrate the block.
|
|
let tx_infos = self
|
|
.receipt_provider
|
|
.receipts_from_block(&block)
|
|
.await
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.map(|(signed_tx, receipt)| TransactionInfo::new(&receipt, signed_tx))
|
|
.collect::<Vec<_>>();
|
|
|
|
eth_block.transactions = HashesOrTransactionInfos::TransactionInfos(tx_infos);
|
|
}
|
|
|
|
Some(eth_block)
|
|
},
|
|
Err(err) => {
|
|
log::error!(target: LOG_TARGET, "Failed to get Ethereum block for hash {:?}: {err:?}", block.hash());
|
|
None
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Get the chain ID.
|
|
pub fn chain_id(&self) -> u64 {
|
|
self.chain_id
|
|
}
|
|
|
|
/// Get the Max Block Weight.
|
|
pub fn max_block_weight(&self) -> Weight {
|
|
self.max_block_weight
|
|
}
|
|
|
|
/// Get the block notifier, if automine is enabled or Self::create_block_notifier was called.
|
|
pub fn block_notifier(&self) -> Option<tokio::sync::broadcast::Sender<H256>> {
|
|
self.block_notifier.clone()
|
|
}
|
|
|
|
/// Get the logs matching the given filter.
|
|
pub async fn logs(&self, filter: Option<Filter>) -> Result<Vec<Log>, ClientError> {
|
|
let logs =
|
|
self.receipt_provider.logs(filter).await.map_err(ClientError::LogFilterFailed)?;
|
|
Ok(logs)
|
|
}
|
|
|
|
pub async fn fee_history(
|
|
&self,
|
|
block_count: u32,
|
|
latest_block: BlockNumberOrTag,
|
|
reward_percentiles: Option<Vec<f64>>,
|
|
) -> Result<FeeHistoryResult, ClientError> {
|
|
let Some(latest_block) = self.block_by_number_or_tag(&latest_block).await? else {
|
|
return Err(ClientError::BlockNotFound);
|
|
};
|
|
|
|
self.fee_history_provider
|
|
.fee_history(block_count, latest_block.number(), reward_percentiles)
|
|
.await
|
|
}
|
|
|
|
/// Check if automine is enabled.
|
|
pub fn is_automine(&self) -> bool {
|
|
self.automine
|
|
}
|
|
|
|
/// Get the automine status from the node.
|
|
pub async fn get_automine(&self) -> bool {
|
|
get_automine(&self.rpc_client).await
|
|
}
|
|
}
|
|
|
|
fn to_hex(bytes: impl AsRef<[u8]>) -> String {
|
|
format!("0x{}", hex::encode(bytes.as_ref()))
|
|
}
|