feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
// 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.
|
||||
mod debug_apis;
|
||||
pub use debug_apis::*;
|
||||
|
||||
mod execution_apis;
|
||||
pub use execution_apis::*;
|
||||
|
||||
mod health_api;
|
||||
pub use health_api::*;
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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::*;
|
||||
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
|
||||
|
||||
/// Debug Ethererum JSON-RPC apis.
|
||||
#[rpc(server, client)]
|
||||
pub trait DebugRpc {
|
||||
/// Returns the tracing of the execution of a specific block using its number.
|
||||
///
|
||||
/// ## References
|
||||
///
|
||||
/// - <https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtraceblockbynumber>
|
||||
#[method(name = "debug_traceBlockByNumber")]
|
||||
async fn trace_block_by_number(
|
||||
&self,
|
||||
block: BlockNumberOrTag,
|
||||
tracer_config: TracerConfig,
|
||||
) -> RpcResult<Vec<TransactionTrace>>;
|
||||
|
||||
/// Returns a transaction's traces by replaying it.
|
||||
///
|
||||
/// ## References
|
||||
///
|
||||
/// - <https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtracetransaction>
|
||||
#[method(name = "debug_traceTransaction")]
|
||||
async fn trace_transaction(
|
||||
&self,
|
||||
transaction_hash: H256,
|
||||
tracer_config: TracerConfig,
|
||||
) -> RpcResult<Trace>;
|
||||
|
||||
/// Dry run a call and returns the transaction's traces.
|
||||
///
|
||||
/// ## References
|
||||
///
|
||||
/// - <https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtracecall>
|
||||
#[method(name = "debug_traceCall")]
|
||||
async fn trace_call(
|
||||
&self,
|
||||
transaction: GenericTransaction,
|
||||
block: BlockNumberOrTagOrHash,
|
||||
tracer_config: TracerConfig,
|
||||
) -> RpcResult<Trace>;
|
||||
|
||||
#[method(name = "debug_getAutomine")]
|
||||
async fn get_automine(&self) -> RpcResult<bool>;
|
||||
}
|
||||
|
||||
pub struct DebugRpcServerImpl {
|
||||
client: client::Client,
|
||||
}
|
||||
|
||||
impl DebugRpcServerImpl {
|
||||
pub fn new(client: client::Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
async fn with_timeout<T>(
|
||||
timeout: Option<core::time::Duration>,
|
||||
fut: impl std::future::Future<Output = Result<T, ClientError>>,
|
||||
) -> RpcResult<T> {
|
||||
if let Some(timeout) = timeout {
|
||||
match tokio::time::timeout(timeout, fut).await {
|
||||
Ok(r) => Ok(r?),
|
||||
Err(_) => Err(ErrorObjectOwned::owned::<String>(
|
||||
-32000,
|
||||
"execution timeout".to_string(),
|
||||
None,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Ok(fut.await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DebugRpcServer for DebugRpcServerImpl {
|
||||
async fn trace_block_by_number(
|
||||
&self,
|
||||
block: BlockNumberOrTag,
|
||||
tracer_config: TracerConfig,
|
||||
) -> RpcResult<Vec<TransactionTrace>> {
|
||||
let TracerConfig { config, timeout } = tracer_config;
|
||||
with_timeout(timeout, self.client.trace_block_by_number(block, config)).await
|
||||
}
|
||||
|
||||
async fn trace_transaction(
|
||||
&self,
|
||||
transaction_hash: H256,
|
||||
tracer_config: TracerConfig,
|
||||
) -> RpcResult<Trace> {
|
||||
let TracerConfig { config, timeout } = tracer_config;
|
||||
with_timeout(timeout, self.client.trace_transaction(transaction_hash, config)).await
|
||||
}
|
||||
|
||||
async fn trace_call(
|
||||
&self,
|
||||
transaction: GenericTransaction,
|
||||
block: BlockNumberOrTagOrHash,
|
||||
tracer_config: TracerConfig,
|
||||
) -> RpcResult<Trace> {
|
||||
let TracerConfig { config, timeout } = tracer_config;
|
||||
with_timeout(timeout, self.client.trace_call(transaction, block, config)).await
|
||||
}
|
||||
|
||||
async fn get_automine(&self) -> RpcResult<bool> {
|
||||
pezsc_service::Result::Ok(self.client.get_automine().await)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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.
|
||||
|
||||
//! Generated JSON-RPC methods.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use crate::*;
|
||||
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
|
||||
|
||||
#[rpc(server, client)]
|
||||
pub trait EthRpc {
|
||||
/// Returns a list of addresses owned by client.
|
||||
#[method(name = "eth_accounts")]
|
||||
async fn accounts(&self) -> RpcResult<Vec<Address>>;
|
||||
|
||||
/// Returns the number of most recent block.
|
||||
#[method(name = "eth_blockNumber")]
|
||||
async fn block_number(&self) -> RpcResult<U256>;
|
||||
|
||||
/// Executes a new message call immediately without creating a transaction on the block chain.
|
||||
#[method(name = "eth_call")]
|
||||
async fn call(
|
||||
&self,
|
||||
transaction: GenericTransaction,
|
||||
block: Option<BlockNumberOrTagOrHash>,
|
||||
) -> RpcResult<Bytes>;
|
||||
|
||||
/// Returns the chain ID of the current network.
|
||||
#[method(name = "eth_chainId")]
|
||||
async fn chain_id(&self) -> RpcResult<U256>;
|
||||
|
||||
/// Generates and returns an estimate of how much gas is necessary to allow the transaction to
|
||||
/// complete.
|
||||
#[method(name = "eth_estimateGas")]
|
||||
async fn estimate_gas(
|
||||
&self,
|
||||
transaction: GenericTransaction,
|
||||
block: Option<BlockNumberOrTag>,
|
||||
) -> RpcResult<U256>;
|
||||
|
||||
/// Returns the current price per gas in wei.
|
||||
#[method(name = "eth_gasPrice")]
|
||||
async fn gas_price(&self) -> RpcResult<U256>;
|
||||
|
||||
/// Returns the balance of the account of given address.
|
||||
#[method(name = "eth_getBalance")]
|
||||
async fn get_balance(&self, address: Address, block: BlockNumberOrTagOrHash)
|
||||
-> RpcResult<U256>;
|
||||
|
||||
/// Returns information about a block by hash.
|
||||
#[method(name = "eth_getBlockByHash")]
|
||||
async fn get_block_by_hash(
|
||||
&self,
|
||||
block_hash: H256,
|
||||
hydrated_transactions: bool,
|
||||
) -> RpcResult<Option<Block>>;
|
||||
|
||||
/// Returns information about a block by number.
|
||||
#[method(name = "eth_getBlockByNumber")]
|
||||
async fn get_block_by_number(
|
||||
&self,
|
||||
block: BlockNumberOrTag,
|
||||
hydrated_transactions: bool,
|
||||
) -> RpcResult<Option<Block>>;
|
||||
|
||||
/// Returns the number of transactions in a block from a block matching the given block hash.
|
||||
#[method(name = "eth_getBlockTransactionCountByHash")]
|
||||
async fn get_block_transaction_count_by_hash(
|
||||
&self,
|
||||
block_hash: Option<H256>,
|
||||
) -> RpcResult<Option<U256>>;
|
||||
|
||||
/// Returns the number of transactions in a block matching the given block number.
|
||||
#[method(name = "eth_getBlockTransactionCountByNumber")]
|
||||
async fn get_block_transaction_count_by_number(
|
||||
&self,
|
||||
block: Option<BlockNumberOrTag>,
|
||||
) -> RpcResult<Option<U256>>;
|
||||
|
||||
/// Returns code at a given address.
|
||||
#[method(name = "eth_getCode")]
|
||||
async fn get_code(&self, address: Address, block: BlockNumberOrTagOrHash) -> RpcResult<Bytes>;
|
||||
|
||||
/// Returns an array of all logs matching filter with given id.
|
||||
#[method(name = "eth_getLogs")]
|
||||
async fn get_logs(&self, filter: Option<Filter>) -> RpcResult<FilterResults>;
|
||||
|
||||
/// Returns the value from a storage position at a given address.
|
||||
#[method(name = "eth_getStorageAt")]
|
||||
async fn get_storage_at(
|
||||
&self,
|
||||
address: Address,
|
||||
storage_slot: U256,
|
||||
block: BlockNumberOrTagOrHash,
|
||||
) -> RpcResult<Bytes>;
|
||||
|
||||
/// Returns information about a transaction by block hash and transaction index position.
|
||||
#[method(name = "eth_getTransactionByBlockHashAndIndex")]
|
||||
async fn get_transaction_by_block_hash_and_index(
|
||||
&self,
|
||||
block_hash: H256,
|
||||
transaction_index: U256,
|
||||
) -> RpcResult<Option<TransactionInfo>>;
|
||||
|
||||
/// Returns information about a transaction by block number and transaction index position.
|
||||
#[method(name = "eth_getTransactionByBlockNumberAndIndex")]
|
||||
async fn get_transaction_by_block_number_and_index(
|
||||
&self,
|
||||
block: BlockNumberOrTag,
|
||||
transaction_index: U256,
|
||||
) -> RpcResult<Option<TransactionInfo>>;
|
||||
|
||||
/// Returns the information about a transaction requested by transaction hash.
|
||||
#[method(name = "eth_getTransactionByHash")]
|
||||
async fn get_transaction_by_hash(
|
||||
&self,
|
||||
transaction_hash: H256,
|
||||
) -> RpcResult<Option<TransactionInfo>>;
|
||||
|
||||
/// Returns the number of transactions sent from an address.
|
||||
#[method(name = "eth_getTransactionCount")]
|
||||
async fn get_transaction_count(
|
||||
&self,
|
||||
address: Address,
|
||||
block: BlockNumberOrTagOrHash,
|
||||
) -> RpcResult<U256>;
|
||||
|
||||
/// Returns the receipt of a transaction by transaction hash.
|
||||
#[method(name = "eth_getTransactionReceipt")]
|
||||
async fn get_transaction_receipt(
|
||||
&self,
|
||||
transaction_hash: H256,
|
||||
) -> RpcResult<Option<ReceiptInfo>>;
|
||||
|
||||
/// Returns the current maxPriorityFeePerGas per gas in wei.
|
||||
#[method(name = "eth_maxPriorityFeePerGas")]
|
||||
async fn max_priority_fee_per_gas(&self) -> RpcResult<U256>;
|
||||
|
||||
/// Submits a raw transaction. For EIP-4844 transactions, the raw form must be the network form.
|
||||
/// This means it includes the blobs, KZG commitments, and KZG proofs.
|
||||
#[method(name = "eth_sendRawTransaction")]
|
||||
async fn send_raw_transaction(&self, transaction: Bytes) -> RpcResult<H256>;
|
||||
|
||||
/// Signs and submits a transaction.
|
||||
#[method(name = "eth_sendTransaction")]
|
||||
async fn send_transaction(&self, transaction: GenericTransaction) -> RpcResult<H256>;
|
||||
|
||||
/// Returns an object with data about the sync status or false.
|
||||
#[method(name = "eth_syncing")]
|
||||
async fn syncing(&self) -> RpcResult<SyncingStatus>;
|
||||
|
||||
/// Returns true when the client is actively listening for network connections, otherwise false
|
||||
#[method(name = "net_listening")]
|
||||
async fn net_listening(&self) -> RpcResult<bool>;
|
||||
|
||||
/// The string value of current network id
|
||||
#[method(name = "net_version")]
|
||||
async fn net_version(&self) -> RpcResult<String>;
|
||||
|
||||
/// The string value of the current client version
|
||||
#[method(name = "web3_clientVersion")]
|
||||
async fn web3_client_version(&self) -> RpcResult<String>;
|
||||
|
||||
/// Returns transaction base fee per gas and effective priority fee per gas for the
|
||||
/// requested/supported block range.
|
||||
///
|
||||
/// Transaction fee history, which is introduced in EIP-1159.
|
||||
#[method(name = "eth_feeHistory")]
|
||||
async fn fee_history(
|
||||
&self,
|
||||
block_count: U256,
|
||||
newest_block: BlockNumberOrTag,
|
||||
reward_percentiles: Option<Vec<f64>>,
|
||||
) -> RpcResult<FeeHistoryResult>;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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.
|
||||
//! Heatlh JSON-RPC methods.
|
||||
|
||||
use crate::*;
|
||||
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
|
||||
use pezsc_rpc_api::system::helpers::Health;
|
||||
|
||||
#[rpc(server, client)]
|
||||
pub trait SystemHealthRpc {
|
||||
/// Proxy the bizinikiwi chain system_health RPC call.
|
||||
#[method(name = "system_health")]
|
||||
async fn system_health(&self) -> RpcResult<Health>;
|
||||
|
||||
///Returns the number of peers currently connected to the client.
|
||||
#[method(name = "net_peerCount")]
|
||||
async fn net_peer_count(&self) -> RpcResult<U64>;
|
||||
}
|
||||
|
||||
pub struct SystemHealthRpcServerImpl {
|
||||
client: client::Client,
|
||||
}
|
||||
|
||||
impl SystemHealthRpcServerImpl {
|
||||
pub fn new(client: client::Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SystemHealthRpcServer for SystemHealthRpcServerImpl {
|
||||
async fn system_health(&self) -> RpcResult<Health> {
|
||||
let (sync_state, health) =
|
||||
tokio::try_join!(self.client.sync_state(), self.client.system_health())?;
|
||||
|
||||
let latest = self.client.latest_block().await.number();
|
||||
|
||||
// Compare against `latest + 1` to avoid a false positive if the health check runs
|
||||
// immediately after a new block is produced but before the cache updates.
|
||||
if sync_state.current_block > latest + 1 {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"Client is out of sync. Current block: {}, latest cache block: {latest}",
|
||||
sync_state.current_block,
|
||||
);
|
||||
return Err(ErrorCode::InternalError.into());
|
||||
}
|
||||
|
||||
Ok(Health {
|
||||
peers: health.peers,
|
||||
is_syncing: health.is_syncing,
|
||||
should_have_peers: health.should_have_peers,
|
||||
})
|
||||
}
|
||||
|
||||
async fn net_peer_count(&self) -> RpcResult<U64> {
|
||||
let health = self.client.system_health().await?;
|
||||
Ok((health.peers as u64).into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// 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::{SubscriptionType, BizinikiwiBlock, BizinikiwiBlockNumber},
|
||||
subxt_client::SrcChainConfig,
|
||||
ClientError,
|
||||
};
|
||||
use jsonrpsee::core::async_trait;
|
||||
use pezsp_core::H256;
|
||||
use std::sync::Arc;
|
||||
use subxt::{backend::legacy::LegacyRpcMethods, OnlineClient};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// BlockInfoProvider cache and retrieves information about blocks.
|
||||
#[async_trait]
|
||||
pub trait BlockInfoProvider: Send + Sync {
|
||||
/// Update the latest block
|
||||
async fn update_latest(&self, block: Arc<BizinikiwiBlock>, subscription_type: SubscriptionType);
|
||||
|
||||
/// Return the latest finalized block.
|
||||
async fn latest_finalized_block(&self) -> Arc<BizinikiwiBlock>;
|
||||
|
||||
/// Return the latest block.
|
||||
async fn latest_block(&self) -> Arc<BizinikiwiBlock>;
|
||||
|
||||
/// Return the latest block number
|
||||
async fn latest_block_number(&self) -> BizinikiwiBlockNumber {
|
||||
return self.latest_block().await.number();
|
||||
}
|
||||
|
||||
/// Get block by block_number.
|
||||
async fn block_by_number(
|
||||
&self,
|
||||
block_number: BizinikiwiBlockNumber,
|
||||
) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError>;
|
||||
|
||||
/// Get block by block hash.
|
||||
async fn block_by_hash(&self, hash: &H256) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError>;
|
||||
}
|
||||
|
||||
/// Provides information about blocks.
|
||||
#[derive(Clone)]
|
||||
pub struct SubxtBlockInfoProvider {
|
||||
/// The latest block.
|
||||
latest_block: Arc<RwLock<Arc<BizinikiwiBlock>>>,
|
||||
|
||||
/// The latest finalized block.
|
||||
latest_finalized_block: Arc<RwLock<Arc<BizinikiwiBlock>>>,
|
||||
|
||||
/// The rpc client, used to fetch blocks not in the cache.
|
||||
rpc: LegacyRpcMethods<SrcChainConfig>,
|
||||
|
||||
/// The api client, used to fetch blocks not in the cache.
|
||||
api: OnlineClient<SrcChainConfig>,
|
||||
}
|
||||
|
||||
impl SubxtBlockInfoProvider {
|
||||
pub async fn new(
|
||||
api: OnlineClient<SrcChainConfig>,
|
||||
rpc: LegacyRpcMethods<SrcChainConfig>,
|
||||
) -> Result<Self, ClientError> {
|
||||
let latest = Arc::new(api.blocks().at_latest().await?);
|
||||
Ok(Self {
|
||||
api,
|
||||
rpc,
|
||||
latest_block: Arc::new(RwLock::new(latest.clone())),
|
||||
latest_finalized_block: Arc::new(RwLock::new(latest)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BlockInfoProvider for SubxtBlockInfoProvider {
|
||||
async fn update_latest(&self, block: Arc<BizinikiwiBlock>, subscription_type: SubscriptionType) {
|
||||
let mut latest = match subscription_type {
|
||||
SubscriptionType::FinalizedBlocks => self.latest_finalized_block.write().await,
|
||||
SubscriptionType::BestBlocks => self.latest_block.write().await,
|
||||
};
|
||||
*latest = block;
|
||||
}
|
||||
|
||||
async fn latest_block(&self) -> Arc<BizinikiwiBlock> {
|
||||
self.latest_block.read().await.clone()
|
||||
}
|
||||
|
||||
async fn latest_finalized_block(&self) -> Arc<BizinikiwiBlock> {
|
||||
self.latest_finalized_block.read().await.clone()
|
||||
}
|
||||
|
||||
async fn block_by_number(
|
||||
&self,
|
||||
block_number: BizinikiwiBlockNumber,
|
||||
) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError> {
|
||||
let latest = self.latest_block().await;
|
||||
if block_number == latest.number() {
|
||||
return Ok(Some(latest));
|
||||
}
|
||||
|
||||
let latest_finalized = self.latest_finalized_block().await;
|
||||
if block_number == latest_finalized.number() {
|
||||
return Ok(Some(latest_finalized));
|
||||
}
|
||||
|
||||
let Some(hash) = self.rpc.chain_get_block_hash(Some(block_number.into())).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match self.api.blocks().at(hash).await {
|
||||
Ok(block) => Ok(Some(Arc::new(block))),
|
||||
Err(subxt::Error::Block(subxt::error::BlockError::NotFound(_))) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn block_by_hash(&self, hash: &H256) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError> {
|
||||
let latest = self.latest_block().await;
|
||||
if hash == &latest.hash() {
|
||||
return Ok(Some(latest));
|
||||
}
|
||||
|
||||
let latest_finalized = self.latest_finalized_block().await;
|
||||
if hash == &latest_finalized.hash() {
|
||||
return Ok(Some(latest_finalized));
|
||||
}
|
||||
|
||||
match self.api.blocks().at(*hash).await {
|
||||
Ok(block) => Ok(Some(Arc::new(block))),
|
||||
Err(subxt::Error::Block(subxt::error::BlockError::NotFound(_))) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::*;
|
||||
use crate::BlockInfo;
|
||||
|
||||
/// A Noop BlockInfoProvider used to test [`db::ReceiptProvider`].
|
||||
pub struct MockBlockInfoProvider;
|
||||
|
||||
pub struct MockBlockInfo {
|
||||
pub number: BizinikiwiBlockNumber,
|
||||
pub hash: H256,
|
||||
}
|
||||
|
||||
impl BlockInfo for MockBlockInfo {
|
||||
fn hash(&self) -> H256 {
|
||||
self.hash
|
||||
}
|
||||
fn number(&self) -> BizinikiwiBlockNumber {
|
||||
self.number
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BlockInfoProvider for MockBlockInfoProvider {
|
||||
async fn update_latest(
|
||||
&self,
|
||||
_block: Arc<BizinikiwiBlock>,
|
||||
_subscription_type: SubscriptionType,
|
||||
) {
|
||||
}
|
||||
|
||||
async fn latest_finalized_block(&self) -> Arc<BizinikiwiBlock> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn latest_block(&self) -> Arc<BizinikiwiBlock> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn latest_block_number(&self) -> BizinikiwiBlockNumber {
|
||||
2u32
|
||||
}
|
||||
|
||||
async fn block_by_number(
|
||||
&self,
|
||||
_block_number: BizinikiwiBlockNumber,
|
||||
) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn block_by_hash(
|
||||
&self,
|
||||
_hash: &H256,
|
||||
) -> Result<Option<Arc<BizinikiwiBlock>>, ClientError> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// 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.
|
||||
//! The Ethereum JSON-RPC server.
|
||||
use crate::{
|
||||
client::{connect, Client, SubscriptionType, BizinikiwiBlockNumber},
|
||||
DebugRpcServer, DebugRpcServerImpl, EthRpcServer, EthRpcServerImpl, ReceiptExtractor,
|
||||
ReceiptProvider, SubxtBlockInfoProvider, SystemHealthRpcServer, SystemHealthRpcServerImpl,
|
||||
LOG_TARGET,
|
||||
};
|
||||
use clap::Parser;
|
||||
use futures::{future::BoxFuture, pin_mut, FutureExt};
|
||||
use jsonrpsee::server::RpcModule;
|
||||
use pezsc_cli::{PrometheusParams, RpcParams, SharedParams, Signals};
|
||||
use pezsc_service::{
|
||||
config::{PrometheusConfig, RpcConfiguration},
|
||||
start_rpc_servers, TaskManager,
|
||||
};
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
|
||||
// Default port if --prometheus-port is not specified
|
||||
const DEFAULT_PROMETHEUS_PORT: u16 = 9616;
|
||||
|
||||
// Default port if --rpc-port is not specified
|
||||
const DEFAULT_RPC_PORT: u16 = 8545;
|
||||
|
||||
const IN_MEMORY_DB: &str = "sqlite::memory:";
|
||||
|
||||
// Parsed command instructions from the command line
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, about, version)]
|
||||
pub struct CliCommand {
|
||||
/// The node url to connect to
|
||||
#[clap(long, default_value = "ws://127.0.0.1:9944")]
|
||||
pub node_rpc_url: String,
|
||||
|
||||
/// The maximum number of blocks to cache in memory.
|
||||
#[clap(long, default_value = "256")]
|
||||
pub cache_size: usize,
|
||||
|
||||
/// Earliest block number to consider when searching for transaction receipts.
|
||||
#[clap(long)]
|
||||
pub earliest_receipt_block: Option<BizinikiwiBlockNumber>,
|
||||
|
||||
/// The database used to store Ethereum transaction hashes.
|
||||
/// This is only useful if the node needs to act as an archive node and respond to Ethereum RPC
|
||||
/// queries for transactions that are not in the in memory cache.
|
||||
#[clap(long, env = "DATABASE_URL", default_value = IN_MEMORY_DB)]
|
||||
pub database_url: String,
|
||||
|
||||
/// If provided, index the last n blocks
|
||||
#[clap(long)]
|
||||
pub index_last_n_blocks: Option<BizinikiwiBlockNumber>,
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[clap(flatten)]
|
||||
pub shared_params: SharedParams,
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[clap(flatten)]
|
||||
pub rpc_params: RpcParams,
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[clap(flatten)]
|
||||
pub prometheus_params: PrometheusParams,
|
||||
}
|
||||
|
||||
/// Initialize the logger
|
||||
#[cfg(not(test))]
|
||||
fn init_logger(params: &SharedParams) -> anyhow::Result<()> {
|
||||
let mut logger = pezsc_cli::LoggerBuilder::new(params.log_filters().join(","));
|
||||
logger
|
||||
.with_log_reloading(params.enable_log_reloading)
|
||||
.with_detailed_output(params.detailed_log_output);
|
||||
|
||||
if let Some(tracing_targets) = ¶ms.tracing_targets {
|
||||
let tracing_receiver = params.tracing_receiver.into();
|
||||
logger.with_profiling(tracing_receiver, tracing_targets);
|
||||
}
|
||||
|
||||
if params.disable_log_color {
|
||||
logger.with_colors(false);
|
||||
}
|
||||
|
||||
logger.init()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_client(
|
||||
tokio_handle: &tokio::runtime::Handle,
|
||||
cache_size: usize,
|
||||
earliest_receipt_block: Option<BizinikiwiBlockNumber>,
|
||||
node_rpc_url: &str,
|
||||
database_url: &str,
|
||||
abort_signal: Signals,
|
||||
) -> anyhow::Result<Client> {
|
||||
let fut = async {
|
||||
let (api, rpc_client, rpc) = connect(node_rpc_url).await?;
|
||||
let block_provider = SubxtBlockInfoProvider::new( api.clone(), rpc.clone()).await?;
|
||||
|
||||
let (pool, keep_latest_n_blocks) = if database_url == IN_MEMORY_DB {
|
||||
log::warn!( target: LOG_TARGET, "💾 Using in-memory database, keeping only {cache_size} blocks in memory");
|
||||
// see sqlite in-memory issue: https://github.com/launchbadge/sqlx/issues/2510
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.idle_timeout(None)
|
||||
.max_lifetime(None)
|
||||
.connect(database_url).await?;
|
||||
|
||||
(pool, Some(cache_size))
|
||||
} else {
|
||||
(SqlitePoolOptions::new().connect(database_url).await?, None)
|
||||
};
|
||||
|
||||
let receipt_extractor = ReceiptExtractor::new(
|
||||
api.clone(),
|
||||
earliest_receipt_block,
|
||||
).await?;
|
||||
|
||||
let receipt_provider = ReceiptProvider::new(
|
||||
pool,
|
||||
block_provider.clone(),
|
||||
receipt_extractor.clone(),
|
||||
keep_latest_n_blocks,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client =
|
||||
Client::new(api, rpc_client, rpc, block_provider, receipt_provider).await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
.fuse();
|
||||
pin_mut!(fut);
|
||||
|
||||
match tokio_handle.block_on(abort_signal.try_until_signal(fut)) {
|
||||
Ok(Ok(client)) => Ok(client),
|
||||
Ok(Err(err)) => Err(err),
|
||||
Err(_) => anyhow::bail!("Process interrupted"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the JSON-RPC server using the given command line arguments.
|
||||
pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
|
||||
let CliCommand {
|
||||
rpc_params,
|
||||
prometheus_params,
|
||||
node_rpc_url,
|
||||
cache_size,
|
||||
database_url,
|
||||
earliest_receipt_block,
|
||||
index_last_n_blocks,
|
||||
shared_params,
|
||||
..
|
||||
} = cmd;
|
||||
|
||||
#[cfg(not(test))]
|
||||
init_logger(&shared_params)?;
|
||||
let is_dev = shared_params.dev;
|
||||
let rpc_addrs: Option<Vec<pezsc_service::config::RpcEndpoint>> = rpc_params
|
||||
.rpc_addr(is_dev, false, 8545)?
|
||||
.map(|addrs| addrs.into_iter().map(Into::into).collect());
|
||||
|
||||
let rpc_config = RpcConfiguration {
|
||||
addr: rpc_addrs,
|
||||
methods: rpc_params.rpc_methods.into(),
|
||||
max_connections: rpc_params.rpc_max_connections,
|
||||
cors: rpc_params.rpc_cors(is_dev)?,
|
||||
max_request_size: rpc_params.rpc_max_request_size,
|
||||
max_response_size: rpc_params.rpc_max_response_size,
|
||||
id_provider: None,
|
||||
max_subs_per_conn: rpc_params.rpc_max_subscriptions_per_connection,
|
||||
port: rpc_params.rpc_port.unwrap_or(DEFAULT_RPC_PORT),
|
||||
message_buffer_capacity: rpc_params.rpc_message_buffer_capacity_per_connection,
|
||||
batch_config: rpc_params.rpc_batch_config()?,
|
||||
rate_limit: rpc_params.rpc_rate_limit,
|
||||
rate_limit_whitelisted_ips: rpc_params.rpc_rate_limit_whitelisted_ips,
|
||||
rate_limit_trust_proxy_headers: rpc_params.rpc_rate_limit_trust_proxy_headers,
|
||||
request_logger_limit: if is_dev { 1024 * 1024 } else { 1024 },
|
||||
};
|
||||
|
||||
let prometheus_config =
|
||||
prometheus_params.prometheus_config(DEFAULT_PROMETHEUS_PORT, "eth-rpc".into());
|
||||
let prometheus_registry = prometheus_config.as_ref().map(|config| &config.registry);
|
||||
|
||||
let tokio_runtime = pezsc_cli::build_runtime()?;
|
||||
let tokio_handle = tokio_runtime.handle();
|
||||
let mut task_manager = TaskManager::new(tokio_handle.clone(), prometheus_registry)?;
|
||||
|
||||
let client = build_client(
|
||||
tokio_handle,
|
||||
cache_size,
|
||||
earliest_receipt_block,
|
||||
&node_rpc_url,
|
||||
&database_url,
|
||||
tokio_runtime.block_on(async { Signals::capture() })?,
|
||||
)?;
|
||||
|
||||
// Prometheus metrics.
|
||||
if let Some(PrometheusConfig { port, registry }) = prometheus_config.clone() {
|
||||
task_manager.spawn_handle().spawn(
|
||||
"prometheus-endpoint",
|
||||
None,
|
||||
prometheus_endpoint::init_prometheus(port, registry).map(drop),
|
||||
);
|
||||
}
|
||||
|
||||
let rpc_server_handle = start_rpc_servers(
|
||||
&rpc_config,
|
||||
prometheus_registry,
|
||||
tokio_handle,
|
||||
|| rpc_module(is_dev, client.clone()),
|
||||
None,
|
||||
)?;
|
||||
|
||||
task_manager
|
||||
.spawn_essential_handle()
|
||||
.spawn("block-subscription", None, async move {
|
||||
let mut futures: Vec<BoxFuture<'_, Result<(), _>>> = vec![
|
||||
Box::pin(client.subscribe_and_cache_new_blocks(SubscriptionType::BestBlocks)),
|
||||
Box::pin(client.subscribe_and_cache_new_blocks(SubscriptionType::FinalizedBlocks)),
|
||||
];
|
||||
|
||||
if let Some(index_last_n_blocks) = index_last_n_blocks {
|
||||
futures.push(Box::pin(client.subscribe_and_cache_blocks(index_last_n_blocks)));
|
||||
}
|
||||
|
||||
if let Err(err) = futures::future::try_join_all(futures).await {
|
||||
panic!("Block subscription task failed: {err:?}",)
|
||||
}
|
||||
});
|
||||
|
||||
task_manager.keep_alive(rpc_server_handle);
|
||||
let signals = tokio_runtime.block_on(async { Signals::capture() })?;
|
||||
tokio_runtime.block_on(signals.run_until_signal(task_manager.future().fuse()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create the JSON-RPC module.
|
||||
fn rpc_module(is_dev: bool, client: Client) -> Result<RpcModule<()>, pezsc_service::Error> {
|
||||
let eth_api = EthRpcServerImpl::new(client.clone())
|
||||
.with_accounts(if is_dev {
|
||||
vec![
|
||||
crate::Account::from(subxt_signer::eth::dev::alith()),
|
||||
crate::Account::from(subxt_signer::eth::dev::baltathar()),
|
||||
crate::Account::from(subxt_signer::eth::dev::charleth()),
|
||||
crate::Account::from(subxt_signer::eth::dev::dorothy()),
|
||||
crate::Account::from(subxt_signer::eth::dev::ethan()),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
})
|
||||
.into_rpc();
|
||||
|
||||
let health_api = SystemHealthRpcServerImpl::new(client.clone()).into_rpc();
|
||||
let debug_api = DebugRpcServerImpl::new(client).into_rpc();
|
||||
|
||||
let mut module = RpcModule::new(());
|
||||
module.merge(eth_api).map_err(|e| pezsc_service::Error::Application(e.into()))?;
|
||||
module.merge(health_api).map_err(|e| pezsc_service::Error::Application(e.into()))?;
|
||||
module.merge(debug_api).map_err(|e| pezsc_service::Error::Application(e.into()))?;
|
||||
Ok(module)
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
// 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.
|
||||
//! 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 pezpallet_revive::{
|
||||
evm::{
|
||||
decode_revert_reason, Block, BlockNumberOrTag, BlockNumberOrTagOrHash, FeeHistoryResult,
|
||||
Filter, GenericTransaction, HashesOrTransactionInfos, Log, ReceiptInfo, SyncingProgress,
|
||||
SyncingStatus, Trace, TransactionSigned, TransactionTrace, H256,
|
||||
},
|
||||
EthTransactError,
|
||||
};
|
||||
use runtime_api::RuntimeApi;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
use pezsp_weights::Weight;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use storage_api::StorageApi;
|
||||
use subxt::{
|
||||
backend::{
|
||||
legacy::{rpc_methods::SystemHealth, LegacyRpcMethods},
|
||||
rpc::{
|
||||
reconnecting_rpc_client::{ExponentialBackoff, RpcClient as ReconnectingRpcClient},
|
||||
RpcClient,
|
||||
},
|
||||
},
|
||||
config::{HashFor, Header},
|
||||
ext::subxt_rpcs::rpc_params,
|
||||
Config, OnlineClient,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// The bizinikiwi block type.
|
||||
pub type BizinikiwiBlock = 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 [`subxt::Error`] wrapper error.
|
||||
#[error(transparent)]
|
||||
SubxtError(#[from] subxt::Error),
|
||||
#[error(transparent)]
|
||||
RpcError(#[from] subxt::ext::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,
|
||||
}
|
||||
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(subxt::Error::Rpc(subxt::error::RpcError::ClientError(
|
||||
subxt::ext::subxt_rpcs::Error::User(err),
|
||||
))) |
|
||||
ClientError::RpcError(subxt::ext::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(
|
||||
node_rpc_url: &str,
|
||||
) -> Result<(OnlineClient<SrcChainConfig>, RpcClient, LegacyRpcMethods<SrcChainConfig>), ClientError>
|
||||
{
|
||||
log::info!(target: LOG_TARGET, "🌐 Connecting to node at: {node_rpc_url} ...");
|
||||
let rpc_client = ReconnectingRpcClient::builder()
|
||||
.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
|
||||
.build(node_rpc_url.to_string())
|
||||
.await?;
|
||||
let rpc_client = RpcClient::new(rpc_client);
|
||||
log::info!(target: LOG_TARGET, "🌟 Connected to node at: {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))
|
||||
}
|
||||
|
||||
/// 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: 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 pallet (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()))
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// 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::Balance,
|
||||
subxt_client::{self, SrcChainConfig},
|
||||
ClientError,
|
||||
};
|
||||
use futures::TryFutureExt;
|
||||
use pezpallet_revive::{
|
||||
evm::{
|
||||
Block as EthBlock, BlockNumberOrTagOrHash, BlockTag, GenericTransaction, ReceiptGasInfo,
|
||||
Trace, H160, U256,
|
||||
},
|
||||
DryRunConfig, EthTransactInfo,
|
||||
};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_timestamp::Timestamp;
|
||||
use subxt::{error::MetadataError, ext::subxt_rpcs::UserError, Error::Metadata, OnlineClient};
|
||||
|
||||
const LOG_TARGET: &str = "eth-rpc::runtime_api";
|
||||
|
||||
/// A Wrapper around subxt Runtime API
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeApi(subxt::runtime_api::RuntimeApi<SrcChainConfig, OnlineClient<SrcChainConfig>>);
|
||||
|
||||
impl RuntimeApi {
|
||||
/// Create a new instance.
|
||||
pub fn new(
|
||||
api: subxt::runtime_api::RuntimeApi<SrcChainConfig, OnlineClient<SrcChainConfig>>,
|
||||
) -> Self {
|
||||
Self(api)
|
||||
}
|
||||
|
||||
/// Get the balance of the given address.
|
||||
pub async fn balance(&self, address: H160) -> Result<U256, ClientError> {
|
||||
let address = address.0.into();
|
||||
let payload = subxt_client::apis().revive_api().balance(address);
|
||||
let balance = self.0.call(payload).await?;
|
||||
Ok(*balance)
|
||||
}
|
||||
|
||||
/// Get the contract storage for the given contract address and key.
|
||||
pub async fn get_storage(
|
||||
&self,
|
||||
contract_address: H160,
|
||||
key: [u8; 32],
|
||||
) -> Result<Option<Vec<u8>>, ClientError> {
|
||||
let contract_address = contract_address.0.into();
|
||||
let payload = subxt_client::apis().revive_api().get_storage(contract_address, key);
|
||||
let result = self.0.call(payload).await?.map_err(|_| ClientError::ContractNotFound)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Dry run a transaction and returns the [`EthTransactInfo`] for the transaction.
|
||||
pub async fn dry_run(
|
||||
&self,
|
||||
tx: GenericTransaction,
|
||||
block: BlockNumberOrTagOrHash,
|
||||
) -> Result<EthTransactInfo<Balance>, ClientError> {
|
||||
let timestamp_override = match block {
|
||||
BlockNumberOrTagOrHash::BlockTag(BlockTag::Pending) =>
|
||||
Some(Timestamp::current().as_millis()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let payload = subxt_client::apis()
|
||||
.revive_api()
|
||||
.eth_transact_with_config(
|
||||
tx.clone().into(),
|
||||
DryRunConfig::new(timestamp_override).into(),
|
||||
)
|
||||
.unvalidated();
|
||||
|
||||
let result = self
|
||||
.0
|
||||
.call(payload)
|
||||
.or_else(|err| async {
|
||||
match err {
|
||||
// This will be hit if subxt metadata (subxt uses the latest finalized block
|
||||
// metadata when the eth-rpc starts) does not contain the new method
|
||||
Metadata(MetadataError::RuntimeMethodNotFound(name)) => {
|
||||
log::debug!(target: LOG_TARGET, "Method {name:?} not found falling back to eth_transact");
|
||||
let payload = subxt_client::apis().revive_api().eth_transact(tx.into());
|
||||
self.0.call(payload).await
|
||||
},
|
||||
// This will be hit if we are trying to hit a block where the runtime did not
|
||||
// have this new runtime `eth_transact_with_config` defined
|
||||
subxt::Error::Rpc(subxt::error::RpcError::ClientError(
|
||||
subxt::ext::subxt_rpcs::Error::User(UserError { message, .. }),
|
||||
)) if message.contains("eth_transact_with_config is not found") => {
|
||||
log::debug!(target: LOG_TARGET, "{message:?} not found falling back to eth_transact");
|
||||
let payload = subxt_client::apis().revive_api().eth_transact(tx.into());
|
||||
self.0.call(payload).await
|
||||
},
|
||||
e => Err(e),
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
log::debug!(target: LOG_TARGET, "Dry run failed {err:?}");
|
||||
Err(ClientError::TransactError(err.0))
|
||||
},
|
||||
Ok(result) => Ok(result.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the nonce of the given address.
|
||||
pub async fn nonce(&self, address: H160) -> Result<U256, ClientError> {
|
||||
let address = address.0.into();
|
||||
let payload = subxt_client::apis().revive_api().nonce(address);
|
||||
let nonce = self.0.call(payload).await?;
|
||||
Ok(nonce.into())
|
||||
}
|
||||
|
||||
/// Get the gas price
|
||||
pub async fn gas_price(&self) -> Result<U256, ClientError> {
|
||||
let payload = subxt_client::apis().revive_api().gas_price();
|
||||
let gas_price = self.0.call(payload).await?;
|
||||
Ok(*gas_price)
|
||||
}
|
||||
|
||||
/// Convert a weight to a fee.
|
||||
pub async fn block_gas_limit(&self) -> Result<U256, ClientError> {
|
||||
let payload = subxt_client::apis().revive_api().block_gas_limit();
|
||||
let gas_limit = self.0.call(payload).await?;
|
||||
Ok(*gas_limit)
|
||||
}
|
||||
|
||||
/// Get the miner address
|
||||
pub async fn block_author(&self) -> Result<H160, ClientError> {
|
||||
let payload = subxt_client::apis().revive_api().block_author();
|
||||
let author = self.0.call(payload).await?;
|
||||
Ok(author)
|
||||
}
|
||||
|
||||
/// Get the trace for the given transaction index in the given block.
|
||||
pub async fn trace_tx(
|
||||
&self,
|
||||
block: pezsp_runtime::generic::Block<
|
||||
pezsp_runtime::generic::Header<u32, pezsp_runtime::traits::BlakeTwo256>,
|
||||
pezsp_runtime::OpaqueExtrinsic,
|
||||
>,
|
||||
transaction_index: u32,
|
||||
tracer_type: crate::TracerType,
|
||||
) -> Result<Trace, ClientError> {
|
||||
let payload = subxt_client::apis()
|
||||
.revive_api()
|
||||
.trace_tx(block.into(), transaction_index, tracer_type.into())
|
||||
.unvalidated();
|
||||
|
||||
let trace = self.0.call(payload).await?.ok_or(ClientError::EthExtrinsicNotFound)?.0;
|
||||
Ok(trace)
|
||||
}
|
||||
|
||||
/// Get the trace for the given block.
|
||||
pub async fn trace_block(
|
||||
&self,
|
||||
block: pezsp_runtime::generic::Block<
|
||||
pezsp_runtime::generic::Header<u32, pezsp_runtime::traits::BlakeTwo256>,
|
||||
pezsp_runtime::OpaqueExtrinsic,
|
||||
>,
|
||||
tracer_type: crate::TracerType,
|
||||
) -> Result<Vec<(u32, Trace)>, ClientError> {
|
||||
let payload = subxt_client::apis()
|
||||
.revive_api()
|
||||
.trace_block(block.into(), tracer_type.into())
|
||||
.unvalidated();
|
||||
|
||||
let traces = self.0.call(payload).await?.into_iter().map(|(idx, t)| (idx, t.0)).collect();
|
||||
Ok(traces)
|
||||
}
|
||||
|
||||
/// Get the trace for the given call.
|
||||
pub async fn trace_call(
|
||||
&self,
|
||||
transaction: GenericTransaction,
|
||||
tracer_type: crate::TracerType,
|
||||
) -> Result<Trace, ClientError> {
|
||||
let payload = subxt_client::apis()
|
||||
.revive_api()
|
||||
.trace_call(transaction.into(), tracer_type.into())
|
||||
.unvalidated();
|
||||
|
||||
let trace = self.0.call(payload).await?.map_err(|err| ClientError::TransactError(err.0))?;
|
||||
Ok(trace.0)
|
||||
}
|
||||
|
||||
/// Get the code of the given address.
|
||||
pub async fn code(&self, address: H160) -> Result<Vec<u8>, ClientError> {
|
||||
let payload = subxt_client::apis().revive_api().code(address);
|
||||
let code = self.0.call(payload).await?;
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
/// Get the current Ethereum block.
|
||||
pub async fn eth_block(&self) -> Result<EthBlock, ClientError> {
|
||||
let payload = subxt_client::apis().revive_api().eth_block();
|
||||
let block = self.0.call(payload).await.inspect_err(|err| {
|
||||
log::debug!(target: LOG_TARGET, "Ethereum block not found, err: {err:?}");
|
||||
})?;
|
||||
Ok(block.0)
|
||||
}
|
||||
|
||||
/// Get the Ethereum block hash for the given block number.
|
||||
pub async fn eth_block_hash(&self, number: U256) -> Result<Option<H256>, ClientError> {
|
||||
let payload = subxt_client::apis().revive_api().eth_block_hash(number.into());
|
||||
let hash = self.0.call(payload).await.inspect_err(|err| {
|
||||
log::debug!(target: LOG_TARGET, "Ethereum block hash for block #{number:?} not found, err: {err:?}");
|
||||
})?;
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
/// Get the receipt data for the current block.
|
||||
pub async fn eth_receipt_data(&self) -> Result<Vec<ReceiptGasInfo>, ClientError> {
|
||||
let payload = subxt_client::apis().revive_api().eth_receipt_data();
|
||||
let receipt_data = self.0.call(payload).await.inspect_err(|err| {
|
||||
log::debug!(target: LOG_TARGET, "Receipt data not found, err: {err:?}");
|
||||
})?;
|
||||
let receipt_data = receipt_data.into_iter().map(|item| item.0).collect();
|
||||
Ok(receipt_data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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::{
|
||||
subxt_client::{
|
||||
self,
|
||||
runtime_types::pezpallet_revive::storage::{AccountType, ContractInfo},
|
||||
SrcChainConfig,
|
||||
},
|
||||
ClientError, H160,
|
||||
};
|
||||
use subxt::{storage::Storage, OnlineClient};
|
||||
|
||||
/// A wrapper around the Bizinikiwi Storage API.
|
||||
#[derive(Clone)]
|
||||
pub struct StorageApi(Storage<SrcChainConfig, OnlineClient<SrcChainConfig>>);
|
||||
|
||||
impl StorageApi {
|
||||
/// Create a new instance of the StorageApi.
|
||||
pub fn new(api: Storage<SrcChainConfig, OnlineClient<SrcChainConfig>>) -> Self {
|
||||
Self(api)
|
||||
}
|
||||
|
||||
/// Get the contract info for the given contract address.
|
||||
pub async fn get_contract_info(
|
||||
&self,
|
||||
contract_address: &H160,
|
||||
) -> Result<ContractInfo, ClientError> {
|
||||
// TODO: remove once subxt is updated
|
||||
let contract_address: subxt::utils::H160 = contract_address.0.into();
|
||||
|
||||
let query = subxt_client::storage().revive().account_info_of(contract_address);
|
||||
let Some(info) = self.0.fetch(&query).await? else {
|
||||
return Err(ClientError::ContractNotFound);
|
||||
};
|
||||
|
||||
let AccountType::Contract(contract_info) = info.account_type else {
|
||||
return Err(ClientError::ContractNotFound);
|
||||
};
|
||||
|
||||
Ok(contract_info)
|
||||
}
|
||||
|
||||
/// Get the contract trie id for the given contract address.
|
||||
pub async fn get_contract_trie_id(&self, address: &H160) -> Result<Vec<u8>, ClientError> {
|
||||
let ContractInfo { trie_id, .. } = self.get_contract_info(address).await?;
|
||||
Ok(trie_id.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// 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.
|
||||
//! Example utilities
|
||||
use crate::{EthRpcClient, ReceiptInfo};
|
||||
use anyhow::Context;
|
||||
use pezpallet_revive::evm::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Transaction builder.
|
||||
pub struct TransactionBuilder<Client: EthRpcClient + Sync + Send> {
|
||||
client: Arc<Client>,
|
||||
signer: Account,
|
||||
value: U256,
|
||||
input: Bytes,
|
||||
to: Option<H160>,
|
||||
nonce: Option<U256>,
|
||||
mutate: Box<dyn FnOnce(&mut TransactionLegacyUnsigned)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SubmittedTransaction<Client: EthRpcClient + Sync + Send> {
|
||||
tx: GenericTransaction,
|
||||
hash: H256,
|
||||
client: Arc<Client>,
|
||||
}
|
||||
|
||||
impl<Client: EthRpcClient + Sync + Send> SubmittedTransaction<Client> {
|
||||
/// Get the hash of the transaction.
|
||||
pub fn hash(&self) -> H256 {
|
||||
self.hash
|
||||
}
|
||||
|
||||
/// The gas sent with the transaction.
|
||||
pub fn gas(&self) -> U256 {
|
||||
self.tx.gas.unwrap()
|
||||
}
|
||||
|
||||
pub fn generic_transaction(&self) -> GenericTransaction {
|
||||
self.tx.clone()
|
||||
}
|
||||
|
||||
/// Wait for the receipt of the transaction.
|
||||
pub async fn wait_for_receipt(&self) -> anyhow::Result<ReceiptInfo> {
|
||||
let hash = self.hash();
|
||||
for _ in 0..30 {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
let receipt = self.client.get_transaction_receipt(hash).await?;
|
||||
if let Some(receipt) = receipt {
|
||||
if receipt.is_success() {
|
||||
assert!(
|
||||
self.gas() > receipt.gas_used,
|
||||
"Gas used should be less than gas estimated."
|
||||
);
|
||||
return Ok(receipt);
|
||||
} else {
|
||||
anyhow::bail!("Transaction failed receipt: {receipt:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("Timeout, failed to get receipt")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Client: EthRpcClient + Send + Sync> TransactionBuilder<Client> {
|
||||
pub fn new(client: &Arc<Client>) -> Self {
|
||||
Self {
|
||||
client: Arc::clone(client),
|
||||
signer: Account::default(),
|
||||
value: U256::zero(),
|
||||
input: Bytes::default(),
|
||||
to: None,
|
||||
nonce: None,
|
||||
mutate: Box::new(|_| {}),
|
||||
}
|
||||
}
|
||||
/// Set the signer.
|
||||
pub fn signer(mut self, signer: Account) -> Self {
|
||||
self.signer = signer;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the value.
|
||||
pub fn value(mut self, value: U256) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the input.
|
||||
pub fn input(mut self, input: Vec<u8>) -> Self {
|
||||
self.input = Bytes(input);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the destination.
|
||||
pub fn to(mut self, to: H160) -> Self {
|
||||
self.to = Some(to);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the nonce.
|
||||
pub fn nonce(mut self, nonce: U256) -> Self {
|
||||
self.nonce = Some(nonce);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a mutation function, that mutates the transaction before sending.
|
||||
pub fn mutate(mut self, mutate: impl FnOnce(&mut TransactionLegacyUnsigned) + 'static) -> Self {
|
||||
self.mutate = Box::new(mutate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Call eth_call to get the result of a view function
|
||||
pub async fn eth_call(self) -> anyhow::Result<Vec<u8>> {
|
||||
let TransactionBuilder { client, signer, value, input, to, .. } = self;
|
||||
|
||||
let from = signer.address();
|
||||
let result = client
|
||||
.call(
|
||||
GenericTransaction {
|
||||
from: Some(from),
|
||||
input: input.into(),
|
||||
value: Some(value),
|
||||
to,
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.with_context(|| "eth_call failed")?;
|
||||
Ok(result.0)
|
||||
}
|
||||
|
||||
/// Send the transaction.
|
||||
pub async fn send(self) -> anyhow::Result<SubmittedTransaction<Client>> {
|
||||
let TransactionBuilder { client, signer, value, input, to, nonce, mutate } = self;
|
||||
|
||||
let from = signer.address();
|
||||
let chain_id = Some(client.chain_id().await?);
|
||||
let gas_price = client.gas_price().await?;
|
||||
let nonce = if let Some(nonce) = nonce {
|
||||
nonce
|
||||
} else {
|
||||
client
|
||||
.get_transaction_count(from, BlockTag::Latest.into())
|
||||
.await
|
||||
.with_context(|| "Failed to fetch account nonce")?
|
||||
};
|
||||
|
||||
let gas = client
|
||||
.estimate_gas(
|
||||
GenericTransaction {
|
||||
from: Some(from),
|
||||
input: input.clone().into(),
|
||||
value: Some(value),
|
||||
gas_price: Some(gas_price),
|
||||
to,
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.with_context(|| "Failed to fetch gas estimate")?;
|
||||
|
||||
println!("Gas estimate: {gas:?}");
|
||||
let mut unsigned_tx = TransactionLegacyUnsigned {
|
||||
gas,
|
||||
nonce,
|
||||
to,
|
||||
value,
|
||||
input,
|
||||
gas_price,
|
||||
chain_id,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
mutate(&mut unsigned_tx);
|
||||
|
||||
let signed_tx = signer.sign_transaction(unsigned_tx.into());
|
||||
let bytes = signed_tx.signed_payload();
|
||||
|
||||
let hash = client
|
||||
.send_raw_transaction(bytes.into())
|
||||
.await
|
||||
.with_context(|| "send_raw_transaction failed")?;
|
||||
|
||||
Ok(SubmittedTransaction {
|
||||
tx: GenericTransaction::from_signed(signed_tx, gas_price, Some(from)),
|
||||
hash,
|
||||
client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dummy_payload_has_correct_len() {
|
||||
let signer = Account::from(subxt_signer::eth::dev::ethan());
|
||||
let unsigned_tx: TransactionUnsigned =
|
||||
TransactionLegacyUnsigned { input: vec![42u8; 100].into(), ..Default::default() }.into();
|
||||
|
||||
let signed_tx = signer.sign_transaction(unsigned_tx.clone());
|
||||
let signed_payload = signed_tx.signed_payload();
|
||||
let unsigned_tx = signed_tx.unsigned();
|
||||
|
||||
let dummy_payload = unsigned_tx.dummy_signed_payload();
|
||||
assert_eq!(dummy_payload.len(), signed_payload.len());
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// 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::BizinikiwiBlockNumber, ClientError};
|
||||
use pezpallet_revive::evm::{Block, FeeHistoryResult, ReceiptInfo};
|
||||
use pezsp_core::U256;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// The size of the fee history cache.
|
||||
const CACHE_SIZE: u32 = 1024;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct FeeHistoryCacheItem {
|
||||
base_fee: u128,
|
||||
gas_used_ratio: f64,
|
||||
rewards: Vec<u128>,
|
||||
}
|
||||
|
||||
/// Manages the fee history cache.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct FeeHistoryProvider {
|
||||
fee_history_cache: Arc<RwLock<BTreeMap<BizinikiwiBlockNumber, FeeHistoryCacheItem>>>,
|
||||
}
|
||||
|
||||
impl FeeHistoryProvider {
|
||||
/// Update the fee history cache with the given block and receipts.
|
||||
pub async fn update_fee_history(&self, block: &Block, receipts: &[ReceiptInfo]) {
|
||||
// Evenly spaced percentile list from 0.0 to 100.0 with a 0.5 resolution.
|
||||
// This means we cache 200 percentile points.
|
||||
// Later in request handling we will approximate by rounding percentiles that
|
||||
// fall in between with `(round(n*2)/2)`.
|
||||
let reward_percentiles: Vec<f64> = (0..=200).map(|i| i as f64 * 0.5).collect();
|
||||
let block_number: BizinikiwiBlockNumber =
|
||||
block.number.try_into().expect("Block number is always valid");
|
||||
|
||||
let base_fee = block.base_fee_per_gas.as_u128();
|
||||
let gas_used = block.gas_used.as_u128();
|
||||
let gas_used_ratio = (gas_used as f64) / (block.gas_limit.as_u128() as f64);
|
||||
let mut result = FeeHistoryCacheItem { base_fee, gas_used_ratio, rewards: vec![] };
|
||||
|
||||
let mut receipts = receipts
|
||||
.iter()
|
||||
.map(|receipt| {
|
||||
let gas_used = receipt.gas_used.as_u128();
|
||||
let effective_reward =
|
||||
receipt.effective_gas_price.as_u128().saturating_sub(base_fee);
|
||||
(gas_used, effective_reward)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
receipts.sort_by(|(_, a), (_, b)| a.cmp(b));
|
||||
|
||||
// Calculate percentile rewards.
|
||||
result.rewards = reward_percentiles
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
let target_gas = (p * gas_used as f64 / 100f64) as u128;
|
||||
let mut sum_gas = 0u128;
|
||||
for (gas_used, reward) in &receipts {
|
||||
sum_gas += gas_used;
|
||||
if target_gas <= sum_gas {
|
||||
return Some(*reward);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut cache = self.fee_history_cache.write().await;
|
||||
if cache.len() >= CACHE_SIZE as usize {
|
||||
cache.pop_first();
|
||||
}
|
||||
cache.insert(block_number, result);
|
||||
}
|
||||
|
||||
/// Get the fee history for the given block range.
|
||||
pub async fn fee_history(
|
||||
&self,
|
||||
block_count: u32,
|
||||
highest: BizinikiwiBlockNumber,
|
||||
reward_percentiles: Option<Vec<f64>>,
|
||||
) -> Result<FeeHistoryResult, ClientError> {
|
||||
let block_count = block_count.min(CACHE_SIZE);
|
||||
|
||||
let cache = self.fee_history_cache.read().await;
|
||||
let Some(lowest_in_cache) = cache.first_key_value().map(|(k, _)| *k) else {
|
||||
return Ok(FeeHistoryResult {
|
||||
oldest_block: U256::zero(),
|
||||
base_fee_per_gas: vec![],
|
||||
gas_used_ratio: vec![],
|
||||
reward: vec![],
|
||||
});
|
||||
};
|
||||
|
||||
let lowest = highest.saturating_sub(block_count.saturating_sub(1)).max(lowest_in_cache);
|
||||
|
||||
let mut response = FeeHistoryResult {
|
||||
oldest_block: U256::from(lowest),
|
||||
base_fee_per_gas: Vec::new(),
|
||||
gas_used_ratio: Vec::new(),
|
||||
reward: Default::default(),
|
||||
};
|
||||
|
||||
let rewards = &mut response.reward;
|
||||
// Iterate over the requested block range.
|
||||
for n in lowest..=highest {
|
||||
if let Some(block) = cache.get(&n) {
|
||||
response.base_fee_per_gas.push(U256::from(block.base_fee));
|
||||
response.gas_used_ratio.push(block.gas_used_ratio);
|
||||
// If the request includes reward percentiles, get them from the cache.
|
||||
if let Some(ref requested_percentiles) = reward_percentiles {
|
||||
let mut block_rewards = Vec::new();
|
||||
// Resolution is half a point. I.e. 1.0,1.5
|
||||
let resolution_per_percentile: f64 = 2.0;
|
||||
// Get cached reward for each provided percentile.
|
||||
for p in requested_percentiles {
|
||||
// Find the cache index from the user percentile.
|
||||
let p = p.clamp(0.0, 100.0);
|
||||
let index = ((p.round() / 2f64) * 2f64) * resolution_per_percentile;
|
||||
// Get and push the reward.
|
||||
let reward = if let Some(r) = block.rewards.get(index as usize) {
|
||||
U256::from(*r)
|
||||
} else {
|
||||
U256::zero()
|
||||
};
|
||||
block_rewards.push(reward);
|
||||
}
|
||||
// Push block rewards.
|
||||
if !block_rewards.is_empty() {
|
||||
rewards.push(block_rewards);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next block base fee, use constant value for now
|
||||
let base_fee = cache
|
||||
.last_key_value()
|
||||
.map(|(_, block)| U256::from(block.base_fee))
|
||||
.unwrap_or_default();
|
||||
response.base_fee_per_gas.push(base_fee);
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_fee_history() {
|
||||
let block = Block {
|
||||
number: U256::from(200u64),
|
||||
base_fee_per_gas: U256::from(1000u64),
|
||||
gas_used: U256::from(600u64),
|
||||
gas_limit: U256::from(1200u64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let receipts = vec![
|
||||
ReceiptInfo {
|
||||
gas_used: U256::from(200u64),
|
||||
effective_gas_price: U256::from(1200u64),
|
||||
..Default::default()
|
||||
},
|
||||
ReceiptInfo {
|
||||
gas_used: U256::from(200u64),
|
||||
effective_gas_price: U256::from(1100u64),
|
||||
..Default::default()
|
||||
},
|
||||
ReceiptInfo {
|
||||
gas_used: U256::from(200u64),
|
||||
effective_gas_price: U256::from(1050u64),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
let provider = FeeHistoryProvider { fee_history_cache: Arc::new(RwLock::new(BTreeMap::new())) };
|
||||
provider.update_fee_history(&block, &receipts).await;
|
||||
|
||||
let fee_history_result =
|
||||
provider.fee_history(1, 200, Some(vec![0.0f64, 50.0, 100.0])).await.unwrap();
|
||||
|
||||
let expected_result = FeeHistoryResult {
|
||||
oldest_block: U256::from(200),
|
||||
base_fee_per_gas: vec![U256::from(1000), U256::from(1000)],
|
||||
gas_used_ratio: vec![0.5f64],
|
||||
reward: vec![vec![U256::from(50), U256::from(100), U256::from(200)]],
|
||||
};
|
||||
assert_eq!(fee_history_result, expected_result);
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
// 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.
|
||||
//! The [`EthRpcServer`] RPC server implementation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use client::ClientError;
|
||||
use jsonrpsee::{
|
||||
core::{async_trait, RpcResult},
|
||||
types::{ErrorCode, ErrorObjectOwned},
|
||||
};
|
||||
use pezpallet_revive::evm::*;
|
||||
use pezsp_core::{keccak_256, H160, H256, U256};
|
||||
use thiserror::Error;
|
||||
use tokio::time::Duration;
|
||||
|
||||
pub mod cli;
|
||||
pub mod client;
|
||||
pub mod example;
|
||||
pub mod subxt_client;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod block_info_provider;
|
||||
pub use block_info_provider::*;
|
||||
|
||||
mod receipt_provider;
|
||||
pub use receipt_provider::*;
|
||||
|
||||
mod fee_history_provider;
|
||||
pub use fee_history_provider::*;
|
||||
|
||||
mod receipt_extractor;
|
||||
pub use receipt_extractor::*;
|
||||
|
||||
mod apis;
|
||||
pub use apis::*;
|
||||
|
||||
pub const LOG_TARGET: &str = "eth-rpc";
|
||||
|
||||
/// An EVM RPC server implementation.
|
||||
pub struct EthRpcServerImpl {
|
||||
/// The client used to interact with the bizinikiwi node.
|
||||
client: client::Client,
|
||||
|
||||
/// The accounts managed by the server.
|
||||
accounts: Vec<Account>,
|
||||
}
|
||||
|
||||
impl EthRpcServerImpl {
|
||||
/// Creates a new [`EthRpcServerImpl`].
|
||||
pub fn new(client: client::Client) -> Self {
|
||||
Self { client, accounts: vec![] }
|
||||
}
|
||||
|
||||
/// Sets the accounts managed by the server.
|
||||
pub fn with_accounts(mut self, accounts: Vec<Account>) -> Self {
|
||||
self.accounts = accounts;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The error type for the EVM RPC server.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum EthRpcError {
|
||||
/// A [`ClientError`] wrapper error.
|
||||
#[error("Client error: {0}")]
|
||||
ClientError(#[from] ClientError),
|
||||
/// A [`rlp::DecoderError`] wrapper error.
|
||||
#[error("Decoding error: {0}")]
|
||||
RlpError(#[from] rlp::DecoderError),
|
||||
/// A Decimals conversion error.
|
||||
#[error("Conversion error")]
|
||||
ConversionError,
|
||||
/// An invalid signature error.
|
||||
#[error("Invalid signature")]
|
||||
InvalidSignature,
|
||||
/// The account was not found at the given address
|
||||
#[error("Account not found for address {0:?}")]
|
||||
AccountNotFound(H160),
|
||||
/// Received an invalid transaction
|
||||
#[error("Invalid transaction")]
|
||||
InvalidTransaction,
|
||||
/// Received an invalid transaction
|
||||
#[error("Invalid transaction {0:?}")]
|
||||
TransactionTypeNotSupported(Byte),
|
||||
}
|
||||
|
||||
// TODO use https://eips.ethereum.org/EIPS/eip-1474#error-codes
|
||||
impl From<EthRpcError> for ErrorObjectOwned {
|
||||
fn from(value: EthRpcError) -> Self {
|
||||
match value {
|
||||
EthRpcError::ClientError(err) => Self::from(err),
|
||||
_ => Self::owned::<String>(ErrorCode::InvalidRequest.code(), value.to_string(), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EthRpcServer for EthRpcServerImpl {
|
||||
async fn net_version(&self) -> RpcResult<String> {
|
||||
Ok(self.client.chain_id().to_string())
|
||||
}
|
||||
|
||||
async fn net_listening(&self) -> RpcResult<bool> {
|
||||
let syncing = self.client.syncing().await?;
|
||||
let listening = matches!(syncing, SyncingStatus::Bool(false));
|
||||
Ok(listening)
|
||||
}
|
||||
|
||||
async fn syncing(&self) -> RpcResult<SyncingStatus> {
|
||||
Ok(self.client.syncing().await?)
|
||||
}
|
||||
|
||||
async fn block_number(&self) -> RpcResult<U256> {
|
||||
let number = self.client.block_number().await?;
|
||||
Ok(number.into())
|
||||
}
|
||||
|
||||
async fn get_transaction_receipt(
|
||||
&self,
|
||||
transaction_hash: H256,
|
||||
) -> RpcResult<Option<ReceiptInfo>> {
|
||||
let receipt = self.client.receipt(&transaction_hash).await;
|
||||
Ok(receipt)
|
||||
}
|
||||
|
||||
async fn estimate_gas(
|
||||
&self,
|
||||
transaction: GenericTransaction,
|
||||
block: Option<BlockNumberOrTag>,
|
||||
) -> RpcResult<U256> {
|
||||
log::trace!(target: LOG_TARGET, "estimate_gas transaction={transaction:?} block={block:?}");
|
||||
let block = block.unwrap_or_default();
|
||||
let hash = self.client.block_hash_for_tag(block.clone().into()).await?;
|
||||
let runtime_api = self.client.runtime_api(hash);
|
||||
let dry_run = runtime_api.dry_run(transaction, block.into()).await?;
|
||||
log::trace!(target: LOG_TARGET, "estimate_gas result={dry_run:?}");
|
||||
Ok(dry_run.eth_gas)
|
||||
}
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
transaction: GenericTransaction,
|
||||
block: Option<BlockNumberOrTagOrHash>,
|
||||
) -> RpcResult<Bytes> {
|
||||
let block = block.unwrap_or_default();
|
||||
let hash = self.client.block_hash_for_tag(block.clone()).await?;
|
||||
let runtime_api = self.client.runtime_api(hash);
|
||||
let dry_run = runtime_api.dry_run(transaction, block).await?;
|
||||
Ok(dry_run.data.into())
|
||||
}
|
||||
|
||||
async fn send_raw_transaction(&self, transaction: Bytes) -> RpcResult<H256> {
|
||||
let hash = H256(keccak_256(&transaction.0));
|
||||
log::trace!(target: LOG_TARGET, "send_raw_transaction transaction: {transaction:?} ethereum_hash: {hash:?}");
|
||||
let call = subxt_client::tx().revive().eth_transact(transaction.0);
|
||||
|
||||
// Subscribe to new block only when automine is enabled.
|
||||
let receiver = self.client.block_notifier().map(|sender| sender.subscribe());
|
||||
|
||||
// Submit the transaction
|
||||
let bizinikiwi_hash = self.client.submit(call).await.map_err(|err| {
|
||||
log::trace!(target: LOG_TARGET, "send_raw_transaction ethereum_hash: {hash:?} failed: {err:?}");
|
||||
err
|
||||
})?;
|
||||
|
||||
log::trace!(target: LOG_TARGET, "send_raw_transaction ethereum_hash: {hash:?} bizinikiwi_hash: {bizinikiwi_hash:?}");
|
||||
|
||||
// Wait for the transaction to be included in a block if automine is enabled
|
||||
if let Some(mut receiver) = receiver {
|
||||
if let Err(err) = tokio::time::timeout(Duration::from_millis(500), async {
|
||||
loop {
|
||||
if let Ok(block_hash) = receiver.recv().await {
|
||||
let Ok(Some(block)) = self.client.block_by_hash(&block_hash).await else {
|
||||
log::debug!(target: LOG_TARGET, "Could not find the block with the received hash: {hash:?}.");
|
||||
continue
|
||||
};
|
||||
let Some(evm_block) = self.client.evm_block(block, false).await else {
|
||||
log::debug!(target: LOG_TARGET, "Failed to get the EVM block for bizinikiwi block with hash: {hash:?}");
|
||||
continue
|
||||
};
|
||||
if evm_block.transactions.contains_tx(hash) {
|
||||
log::debug!(target: LOG_TARGET, "{hash:} was included in a block");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
log::debug!(target: LOG_TARGET, "timeout waiting for new block: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(target: LOG_TARGET, "send_raw_transaction hash: {hash:?}");
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
async fn send_transaction(&self, mut transaction: GenericTransaction) -> RpcResult<H256> {
|
||||
log::debug!(target: LOG_TARGET, "{transaction:#?}");
|
||||
|
||||
let Some(from) = transaction.from else {
|
||||
log::debug!(target: LOG_TARGET, "Transaction must have a sender");
|
||||
return Err(EthRpcError::InvalidTransaction.into());
|
||||
};
|
||||
|
||||
let account = self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| account.address() == from)
|
||||
.ok_or(EthRpcError::AccountNotFound(from))?;
|
||||
|
||||
if transaction.gas.is_none() {
|
||||
transaction.gas = Some(self.estimate_gas(transaction.clone(), None).await?);
|
||||
}
|
||||
|
||||
if transaction.gas_price.is_none() {
|
||||
transaction.gas_price = Some(self.gas_price().await?);
|
||||
}
|
||||
|
||||
if transaction.nonce.is_none() {
|
||||
transaction.nonce =
|
||||
Some(self.get_transaction_count(from, BlockTag::Latest.into()).await?);
|
||||
}
|
||||
|
||||
if transaction.chain_id.is_none() {
|
||||
transaction.chain_id = Some(self.chain_id().await?);
|
||||
}
|
||||
|
||||
let tx = transaction.try_into_unsigned().map_err(|_| EthRpcError::InvalidTransaction)?;
|
||||
let payload = account.sign_transaction(tx).signed_payload();
|
||||
self.send_raw_transaction(Bytes(payload)).await
|
||||
}
|
||||
|
||||
async fn get_block_by_hash(
|
||||
&self,
|
||||
block_hash: H256,
|
||||
hydrated_transactions: bool,
|
||||
) -> RpcResult<Option<Block>> {
|
||||
let Some(block) = self.client.block_by_ethereum_hash(&block_hash).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let block = self.client.evm_block(block, hydrated_transactions).await;
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
async fn get_balance(&self, address: H160, block: BlockNumberOrTagOrHash) -> RpcResult<U256> {
|
||||
let hash = self.client.block_hash_for_tag(block).await?;
|
||||
let runtime_api = self.client.runtime_api(hash);
|
||||
let balance = runtime_api.balance(address).await?;
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
async fn chain_id(&self) -> RpcResult<U256> {
|
||||
Ok(self.client.chain_id().into())
|
||||
}
|
||||
|
||||
async fn gas_price(&self) -> RpcResult<U256> {
|
||||
let hash = self.client.block_hash_for_tag(BlockTag::Latest.into()).await?;
|
||||
let runtime_api = self.client.runtime_api(hash);
|
||||
Ok(runtime_api.gas_price().await?)
|
||||
}
|
||||
|
||||
async fn max_priority_fee_per_gas(&self) -> RpcResult<U256> {
|
||||
// We do not support tips. Hence the recommended priority fee is
|
||||
// always zero. The effective gas price will always be the base price.
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn get_code(&self, address: H160, block: BlockNumberOrTagOrHash) -> RpcResult<Bytes> {
|
||||
let hash = self.client.block_hash_for_tag(block).await?;
|
||||
let code = self.client.runtime_api(hash).code(address).await?;
|
||||
Ok(code.into())
|
||||
}
|
||||
|
||||
async fn accounts(&self) -> RpcResult<Vec<H160>> {
|
||||
Ok(self.accounts.iter().map(|account| account.address()).collect())
|
||||
}
|
||||
|
||||
async fn get_block_by_number(
|
||||
&self,
|
||||
block_number: BlockNumberOrTag,
|
||||
hydrated_transactions: bool,
|
||||
) -> RpcResult<Option<Block>> {
|
||||
let Some(block) = self.client.block_by_number_or_tag(&block_number).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let block = self.client.evm_block(block, hydrated_transactions).await;
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
async fn get_block_transaction_count_by_hash(
|
||||
&self,
|
||||
block_hash: Option<H256>,
|
||||
) -> RpcResult<Option<U256>> {
|
||||
let block_hash = if let Some(block_hash) = block_hash {
|
||||
block_hash
|
||||
} else {
|
||||
self.client.latest_block().await.hash()
|
||||
};
|
||||
|
||||
let Some(bizinikiwi_hash) = self.client.resolve_bizinikiwi_hash(&block_hash).await else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(self.client.receipts_count_per_block(&bizinikiwi_hash).await.map(U256::from))
|
||||
}
|
||||
|
||||
async fn get_block_transaction_count_by_number(
|
||||
&self,
|
||||
block: Option<BlockNumberOrTag>,
|
||||
) -> RpcResult<Option<U256>> {
|
||||
let bizinikiwi_hash = if let Some(block) = self
|
||||
.client
|
||||
.block_by_number_or_tag(&block.unwrap_or_else(|| BlockTag::Latest.into()))
|
||||
.await?
|
||||
{
|
||||
block.hash()
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(self.client.receipts_count_per_block(&bizinikiwi_hash).await.map(U256::from))
|
||||
}
|
||||
|
||||
async fn get_logs(&self, filter: Option<Filter>) -> RpcResult<FilterResults> {
|
||||
let logs = self.client.logs(filter).await?;
|
||||
Ok(FilterResults::Logs(logs))
|
||||
}
|
||||
|
||||
async fn get_storage_at(
|
||||
&self,
|
||||
address: H160,
|
||||
storage_slot: U256,
|
||||
block: BlockNumberOrTagOrHash,
|
||||
) -> RpcResult<Bytes> {
|
||||
let hash = self.client.block_hash_for_tag(block).await?;
|
||||
let runtime_api = self.client.runtime_api(hash);
|
||||
let bytes = runtime_api.get_storage(address, storage_slot.to_big_endian()).await?;
|
||||
Ok(bytes.unwrap_or_default().into())
|
||||
}
|
||||
|
||||
async fn get_transaction_by_block_hash_and_index(
|
||||
&self,
|
||||
block_hash: H256,
|
||||
transaction_index: U256,
|
||||
) -> RpcResult<Option<TransactionInfo>> {
|
||||
let Some(bizinikiwi_block_hash) = self.client.resolve_bizinikiwi_hash(&block_hash).await
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.get_transaction_by_bizinikiwi_block_hash_and_index(
|
||||
bizinikiwi_block_hash,
|
||||
transaction_index,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_transaction_by_block_number_and_index(
|
||||
&self,
|
||||
block: BlockNumberOrTag,
|
||||
transaction_index: U256,
|
||||
) -> RpcResult<Option<TransactionInfo>> {
|
||||
let Some(block) = self.client.block_by_number_or_tag(&block).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.get_transaction_by_bizinikiwi_block_hash_and_index(block.hash(), transaction_index)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_transaction_by_hash(
|
||||
&self,
|
||||
transaction_hash: H256,
|
||||
) -> RpcResult<Option<TransactionInfo>> {
|
||||
let receipt = self.client.receipt(&transaction_hash).await;
|
||||
let signed_tx = self.client.signed_tx_by_hash(&transaction_hash).await;
|
||||
if let (Some(receipt), Some(signed_tx)) = (receipt, signed_tx) {
|
||||
return Ok(Some(TransactionInfo::new(&receipt, signed_tx)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn get_transaction_count(
|
||||
&self,
|
||||
address: H160,
|
||||
block: BlockNumberOrTagOrHash,
|
||||
) -> RpcResult<U256> {
|
||||
let hash = self.client.block_hash_for_tag(block).await?;
|
||||
let runtime_api = self.client.runtime_api(hash);
|
||||
let nonce = runtime_api.nonce(address).await?;
|
||||
Ok(nonce)
|
||||
}
|
||||
|
||||
async fn web3_client_version(&self) -> RpcResult<String> {
|
||||
let git_revision = env!("GIT_REVISION");
|
||||
let rustc_version = env!("RUSTC_VERSION");
|
||||
let target = env!("TARGET");
|
||||
Ok(format!("eth-rpc/{git_revision}/{target}/{rustc_version}"))
|
||||
}
|
||||
|
||||
async fn fee_history(
|
||||
&self,
|
||||
block_count: U256,
|
||||
newest_block: BlockNumberOrTag,
|
||||
reward_percentiles: Option<Vec<f64>>,
|
||||
) -> RpcResult<FeeHistoryResult> {
|
||||
let block_count: u32 = block_count.try_into().map_err(|_| EthRpcError::ConversionError)?;
|
||||
let result = self.client.fee_history(block_count, newest_block, reward_percentiles).await?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl EthRpcServerImpl {
|
||||
async fn get_transaction_by_bizinikiwi_block_hash_and_index(
|
||||
&self,
|
||||
bizinikiwi_block_hash: H256,
|
||||
transaction_index: U256,
|
||||
) -> RpcResult<Option<TransactionInfo>> {
|
||||
let Some(receipt) = self
|
||||
.client
|
||||
.receipt_by_hash_and_index(
|
||||
&bizinikiwi_block_hash,
|
||||
transaction_index.try_into().map_err(|_| EthRpcError::ConversionError)?,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(signed_tx) = self.client.signed_tx_by_hash(&receipt.transaction_hash).await else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(TransactionInfo::new(&receipt, signed_tx)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.
|
||||
//! The Ethereum JSON-RPC server.
|
||||
use clap::Parser;
|
||||
use pezpallet_revive_eth_rpc::cli;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cmd = cli::CliCommand::parse();
|
||||
cli::run(cmd)
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// 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 pezpallet_revive::{
|
||||
create1,
|
||||
evm::{GenericTransaction, Log, ReceiptGasInfo, ReceiptInfo, TransactionSigned, H256, U256},
|
||||
};
|
||||
use pezsp_core::keccak_256;
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
use subxt::{blocks::ExtrinsicDetails, OnlineClient};
|
||||
|
||||
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: subxt::blocks::ExtrinsicDetails<SrcChainConfig, 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 pallet 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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
// 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.
|
||||
//! The generated subxt client.
|
||||
//! Generated against a bizinikiwi chain configured with [`pezpallet_revive`] using:
|
||||
//! subxt metadata --url ws://localhost:9944 -o rpc/revive_chain.scale
|
||||
pub use subxt::config::PolkadotConfig as SrcChainConfig;
|
||||
|
||||
#[subxt::subxt(
|
||||
runtime_metadata_path = "revive_chain.scale",
|
||||
// TODO remove once subxt use the same U256 type
|
||||
substitute_type(
|
||||
path = "primitive_types::U256",
|
||||
with = "::subxt::utils::Static<::pezsp_core::U256>"
|
||||
),
|
||||
|
||||
substitute_type(
|
||||
path = "pezsp_runtime::generic::block::Block<A, B, C, D, E>",
|
||||
with = "::subxt::utils::Static<::pezsp_runtime::generic::Block<
|
||||
::pezsp_runtime::generic::Header<u32, pezsp_runtime::traits::BlakeTwo256>,
|
||||
::pezsp_runtime::OpaqueExtrinsic
|
||||
>>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::evm::api::debug_rpc_types::Trace",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::evm::Trace>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::evm::api::debug_rpc_types::TracerType",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::evm::TracerType>"
|
||||
),
|
||||
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::evm::api::rpc_types_gen::GenericTransaction",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::evm::GenericTransaction>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::evm::api::rpc_types::DryRunConfig<M>",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::evm::DryRunConfig<M>>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::primitives::EthTransactInfo<B>",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::EthTransactInfo<B>>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::primitives::EthTransactError",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::EthTransactError>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::primitives::ExecReturnValue",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::ExecReturnValue>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezsp_weights::weight_v2::Weight",
|
||||
with = "::subxt::utils::Static<::pezsp_weights::Weight>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::evm::api::rpc_types_gen::Block",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::evm::Block>"
|
||||
),
|
||||
substitute_type(
|
||||
path = "pezpallet_revive::evm::block_hash::ReceiptGasInfo",
|
||||
with = "::subxt::utils::Static<::pezpallet_revive::evm::ReceiptGasInfo>"
|
||||
),
|
||||
derive_for_all_types = "codec::Encode, codec::Decode"
|
||||
)]
|
||||
mod src_chain {}
|
||||
pub use src_chain::*;
|
||||
@@ -0,0 +1,787 @@
|
||||
// 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.
|
||||
//! Test the eth-rpc cli with the kitchensink node.
|
||||
//! This only includes basic transaction tests, most of the other tests are in the
|
||||
//! [evm-test-suite](https://github.com/paritytech/evm-test-suite) repository.
|
||||
|
||||
use crate::{
|
||||
cli::{self, CliCommand},
|
||||
client,
|
||||
example::TransactionBuilder,
|
||||
subxt_client::{
|
||||
self, src_chain::runtime_types::pezpallet_revive::primitives::Code, SrcChainConfig,
|
||||
},
|
||||
EthRpcClient,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use jsonrpsee::ws_client::{WsClient, WsClientBuilder};
|
||||
use pezpallet_revive::{
|
||||
create1,
|
||||
evm::{
|
||||
Account, Block, BlockNumberOrTag, BlockNumberOrTagOrHash, BlockTag,
|
||||
HashesOrTransactionInfos, TransactionInfo, H256, U256,
|
||||
},
|
||||
};
|
||||
use std::{sync::Arc, thread};
|
||||
use subxt::{
|
||||
backend::rpc::RpcClient,
|
||||
ext::subxt_rpcs::rpc_params,
|
||||
tx::{SubmittableTransaction, TxStatus},
|
||||
OnlineClient,
|
||||
};
|
||||
|
||||
const LOG_TARGET: &str = "eth-rpc-tests";
|
||||
|
||||
/// Create a websocket client with a 120s timeout.
|
||||
async fn ws_client_with_retry(url: &str) -> WsClient {
|
||||
let timeout = tokio::time::Duration::from_secs(120);
|
||||
tokio::time::timeout(timeout, async {
|
||||
loop {
|
||||
if let Ok(client) = WsClientBuilder::default().build(url).await {
|
||||
return client;
|
||||
} else {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("Hit timeout")
|
||||
}
|
||||
|
||||
struct SharedResources {
|
||||
_node_handle: std::thread::JoinHandle<()>,
|
||||
_rpc_handle: std::thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl SharedResources {
|
||||
fn start() -> Self {
|
||||
// Start revive-dev-node
|
||||
let _node_handle = thread::spawn(move || {
|
||||
if let Err(e) = revive_dev_node::command::run_with_args(vec![
|
||||
"--dev".to_string(),
|
||||
"--rpc-port=45789".to_string(),
|
||||
"-lerror,pezsc_rpc_server=info,runtime::revive=debug".to_string(),
|
||||
]) {
|
||||
panic!("Node exited with error: {e:?}");
|
||||
}
|
||||
});
|
||||
|
||||
// Start the rpc server.
|
||||
let args = CliCommand::parse_from([
|
||||
"--dev",
|
||||
"--rpc-port=45788",
|
||||
"--node-rpc-url=ws://localhost:45789",
|
||||
"--no-prometheus",
|
||||
"-linfo,eth-rpc=debug",
|
||||
]);
|
||||
|
||||
let _rpc_handle = thread::spawn(move || {
|
||||
if let Err(e) = cli::run(args) {
|
||||
panic!("eth-rpc exited with error: {e:?}");
|
||||
}
|
||||
});
|
||||
|
||||
Self { _node_handle, _rpc_handle }
|
||||
}
|
||||
|
||||
async fn client() -> WsClient {
|
||||
ws_client_with_retry("ws://localhost:45788").await
|
||||
}
|
||||
|
||||
fn node_rpc_url() -> &'static str {
|
||||
"ws://localhost:45789"
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! unwrap_call_err(
|
||||
($err:expr) => {
|
||||
match $err.downcast_ref::<jsonrpsee::core::client::Error>().unwrap() {
|
||||
jsonrpsee::core::client::Error::Call(call) => call,
|
||||
_ => panic!("Expected Call error"),
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Helper functions
|
||||
/// Prepare multiple EVM transfer transactions with nonce in descending order
|
||||
async fn prepare_evm_transactions<Client: EthRpcClient + Sync + Send>(
|
||||
client: &Arc<Client>,
|
||||
signer: Account,
|
||||
recipient: pezpallet_revive::evm::Address,
|
||||
amount: U256,
|
||||
count: usize,
|
||||
) -> anyhow::Result<Vec<TransactionBuilder<Client>>> {
|
||||
let start_nonce =
|
||||
client.get_transaction_count(signer.address(), BlockTag::Latest.into()).await?;
|
||||
|
||||
let mut transactions = Vec::new();
|
||||
for i in (0..count).rev() {
|
||||
let nonce = start_nonce.saturating_add(U256::from(i as u64));
|
||||
let tx_builder = TransactionBuilder::new(client)
|
||||
.signer(signer.clone())
|
||||
.nonce(nonce)
|
||||
.value(amount)
|
||||
.to(recipient);
|
||||
|
||||
transactions.push(tx_builder);
|
||||
log::trace!(target: LOG_TARGET, "Prepared EVM transaction {}/{count} with nonce: {nonce:?}", i + 1);
|
||||
}
|
||||
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
/// Prepare multiple Bizinikiwi transfer transactions with sequential nonces
|
||||
async fn prepare_bizinikiwi_transactions(
|
||||
node_client: &OnlineClient<SrcChainConfig>,
|
||||
signer: &subxt_signer::sr25519::Keypair,
|
||||
count: usize,
|
||||
) -> anyhow::Result<Vec<SubmittableTransaction<SrcChainConfig, OnlineClient<SrcChainConfig>>>> {
|
||||
let mut nonce = node_client.tx().account_nonce(&signer.public_key().into()).await?;
|
||||
let mut bizinikiwi_txs = Vec::new();
|
||||
for i in 0..count {
|
||||
let remark_data = format!("Hello from test {}", i);
|
||||
let call = subxt::dynamic::tx(
|
||||
"System",
|
||||
"remark",
|
||||
vec![subxt::dynamic::Value::from_bytes(remark_data.as_bytes())],
|
||||
);
|
||||
|
||||
// Note: Using polkadot config from subxt (external crate)
|
||||
let params = subxt::config::polkadot::PolkadotExtrinsicParamsBuilder::new()
|
||||
.nonce(nonce)
|
||||
.build();
|
||||
|
||||
let tx = node_client.tx().create_signed(&call, signer, params).await?;
|
||||
bizinikiwi_txs.push(tx);
|
||||
log::trace!(target: LOG_TARGET, "Prepared bizinikiwi transaction {i}/{count} with nonce: {nonce}");
|
||||
nonce += 1 as u64;
|
||||
}
|
||||
Ok(bizinikiwi_txs)
|
||||
}
|
||||
|
||||
/// Submit multiple transactions and return them without waiting for receipts
|
||||
async fn submit_evm_transactions<Client: EthRpcClient + Sync + Send>(
|
||||
transactions: Vec<TransactionBuilder<Client>>,
|
||||
) -> anyhow::Result<
|
||||
Vec<(
|
||||
H256,
|
||||
pezpallet_revive::evm::GenericTransaction,
|
||||
crate::example::SubmittedTransaction<Client>,
|
||||
)>,
|
||||
> {
|
||||
let mut submitted_txs = Vec::new();
|
||||
|
||||
for tx_builder in transactions {
|
||||
let tx = tx_builder.send().await?;
|
||||
let hash = tx.hash();
|
||||
let generic_tx = tx.generic_transaction();
|
||||
submitted_txs.push((hash, generic_tx, tx));
|
||||
}
|
||||
|
||||
Ok(submitted_txs)
|
||||
}
|
||||
|
||||
/// Submit bizinikiwi transactions and return futures for waiting
|
||||
async fn submit_bizinikiwi_transactions(
|
||||
bizinikiwi_txs: Vec<SubmittableTransaction<SrcChainConfig, OnlineClient<SrcChainConfig>>>,
|
||||
) -> Vec<impl std::future::Future<Output = Result<(), anyhow::Error>>> {
|
||||
let mut futures = Vec::new();
|
||||
|
||||
for (i, tx) in bizinikiwi_txs.into_iter().enumerate() {
|
||||
let fut = async move {
|
||||
match tx.submit_and_watch().await {
|
||||
Ok(mut progress) => {
|
||||
log::trace!(target: LOG_TARGET, "Bizinikiwi tx {i} submitted");
|
||||
while let Some(status) = progress.next().await {
|
||||
match status {
|
||||
Ok(TxStatus::InFinalizedBlock(block)) |
|
||||
Ok(TxStatus::InBestBlock(block)) => {
|
||||
log::trace!(target: LOG_TARGET,
|
||||
"Bizinikiwi tx {i} included in block {:?}",
|
||||
block.block_hash()
|
||||
);
|
||||
return Ok(());
|
||||
},
|
||||
Err(e) => return Err(anyhow::anyhow!("Bizinikiwi tx {i} error: {e}")),
|
||||
Ok(status) => {
|
||||
log::trace!(target: LOG_TARGET, "Bizinikiwi tx {i} status {:?}", status);
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to get status of submitted bizinikiwi tx {i}, assuming error"
|
||||
))
|
||||
},
|
||||
Err(e) => Err(anyhow::anyhow!("Failed to submit bizinikiwi tx {i}: {e}")),
|
||||
}
|
||||
};
|
||||
futures.push(fut);
|
||||
}
|
||||
|
||||
futures
|
||||
}
|
||||
|
||||
/// Verify all given transaction hashes are in the specified block and accessible via RPC
|
||||
async fn verify_transactions_in_single_block(
|
||||
client: &Arc<WsClient>,
|
||||
block_number: U256,
|
||||
expected_tx_hashes: &[H256],
|
||||
) -> anyhow::Result<()> {
|
||||
// Fetch the block
|
||||
let block = client
|
||||
.get_block_by_number(BlockNumberOrTag::U256(block_number), false)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Block {block_number} should exist"))?;
|
||||
|
||||
let block_tx_hashes = match &block.transactions {
|
||||
HashesOrTransactionInfos::Hashes(hashes) => hashes.clone(),
|
||||
HashesOrTransactionInfos::TransactionInfos(infos) =>
|
||||
infos.iter().map(|info| info.hash).collect(),
|
||||
};
|
||||
|
||||
if let Some(missing_hash) =
|
||||
expected_tx_hashes.iter().find(|hash| !block_tx_hashes.contains(hash))
|
||||
{
|
||||
return Err(anyhow!("Transaction {missing_hash:?} not found in block {block_number}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_all_eth_rpc_tests() -> anyhow::Result<()> {
|
||||
// start node and rpc server
|
||||
let _shared = SharedResources::start();
|
||||
let client = Arc::new(SharedResources::client().await);
|
||||
|
||||
macro_rules! run_tests {
|
||||
($($test:ident),+ $(,)?) => {
|
||||
$(
|
||||
{
|
||||
let test_name = stringify!($test);
|
||||
log::debug!(target: LOG_TARGET, "Running test: {}", test_name);
|
||||
match $test(client.clone()).await {
|
||||
Ok(()) => log::debug!(target: LOG_TARGET, "Test passed: {}", test_name),
|
||||
Err(err) => panic!("Test {} failed: {err:?}", test_name),
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
run_tests!(
|
||||
test_transfer,
|
||||
test_deploy_and_call,
|
||||
test_runtime_api_dry_run_addr_works,
|
||||
test_invalid_transaction,
|
||||
test_evm_blocks_should_match,
|
||||
test_evm_blocks_hydrated_should_match,
|
||||
test_block_hash_for_tag_with_proper_ethereum_block_hash_works,
|
||||
test_block_hash_for_tag_with_invalid_ethereum_block_hash_fails,
|
||||
test_block_hash_for_tag_with_block_number_works,
|
||||
test_block_hash_for_tag_with_block_tags_works,
|
||||
test_multiple_transactions_in_block,
|
||||
test_mixed_evm_bizinikiwi_transactions,
|
||||
test_runtime_pallets_address_upload_code,
|
||||
);
|
||||
|
||||
log::debug!(target: LOG_TARGET, "All tests completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_transfer(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
let ethan = Account::from(subxt_signer::eth::dev::ethan());
|
||||
let initial_balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?;
|
||||
|
||||
let value = 1_000_000_000_000_000_000_000u128.into();
|
||||
let tx = TransactionBuilder::new(&client).value(value).to(ethan.address()).send().await?;
|
||||
|
||||
let receipt = tx.wait_for_receipt().await?;
|
||||
assert_eq!(
|
||||
Some(ethan.address()),
|
||||
receipt.to,
|
||||
"Receipt should have the correct contract address."
|
||||
);
|
||||
|
||||
let balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?;
|
||||
assert_eq!(
|
||||
Some(value),
|
||||
balance.checked_sub(initial_balance),
|
||||
"Ethan {:?} {balance:?} should have increased by {value:?} from {initial_balance}.",
|
||||
ethan.address()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_deploy_and_call(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
let account = Account::default();
|
||||
|
||||
// Balance transfer
|
||||
let ethan = Account::from(subxt_signer::eth::dev::ethan());
|
||||
let initial_balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?;
|
||||
let value = 1_000_000_000_000_000_000_000u128.into();
|
||||
let tx = TransactionBuilder::new(&client).value(value).to(ethan.address()).send().await?;
|
||||
|
||||
let receipt = tx.wait_for_receipt().await?;
|
||||
assert_eq!(
|
||||
Some(ethan.address()),
|
||||
receipt.to,
|
||||
"Receipt should have the correct contract address."
|
||||
);
|
||||
|
||||
let balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?;
|
||||
assert_eq!(
|
||||
Some(value),
|
||||
balance.checked_sub(initial_balance),
|
||||
"Ethan {:?} {balance:?} should have increased by {value:?} from {initial_balance}.",
|
||||
ethan.address()
|
||||
);
|
||||
|
||||
// Deploy contract
|
||||
let data = b"hello world".to_vec();
|
||||
let value = U256::from(5_000_000_000_000u128);
|
||||
let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
|
||||
let input = bytes.into_iter().chain(data.clone()).collect::<Vec<u8>>();
|
||||
let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?;
|
||||
let tx = TransactionBuilder::new(&client).value(value).input(input).send().await?;
|
||||
let receipt = tx.wait_for_receipt().await?;
|
||||
let contract_address = create1(&account.address(), nonce.try_into().unwrap());
|
||||
assert_eq!(
|
||||
Some(contract_address),
|
||||
receipt.contract_address,
|
||||
"Contract should be deployed at {contract_address:?}."
|
||||
);
|
||||
|
||||
let nonce_after_deploy =
|
||||
client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?;
|
||||
|
||||
assert_eq!(nonce_after_deploy - nonce, U256::from(1), "Nonce should have increased by 1");
|
||||
|
||||
let initial_balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?;
|
||||
assert_eq!(
|
||||
value, initial_balance,
|
||||
"Contract {contract_address:?} balance should be the same as the value sent ({value})."
|
||||
);
|
||||
|
||||
// Call contract
|
||||
let tx = TransactionBuilder::new(&client)
|
||||
.value(value)
|
||||
.to(contract_address)
|
||||
.send()
|
||||
.await?;
|
||||
let receipt = tx.wait_for_receipt().await?;
|
||||
|
||||
assert_eq!(
|
||||
Some(contract_address),
|
||||
receipt.to,
|
||||
"Receipt should have the correct contract address {contract_address:?}."
|
||||
);
|
||||
|
||||
let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?;
|
||||
assert_eq!(Some(value), balance.checked_sub(initial_balance), "Contract {contract_address:?} Balance {balance} should have increased from {initial_balance} by {value}.");
|
||||
|
||||
// Balance transfer to contract
|
||||
let initial_balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?;
|
||||
let tx = TransactionBuilder::new(&client)
|
||||
.value(value)
|
||||
.to(contract_address)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
tx.wait_for_receipt().await?;
|
||||
|
||||
let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?;
|
||||
|
||||
assert_eq!(
|
||||
Some(value),
|
||||
balance.checked_sub(initial_balance),
|
||||
"Balance {balance} should have increased from {initial_balance} by {value}."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_runtime_api_dry_run_addr_works(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
let account = Account::default();
|
||||
let origin: [u8; 32] = account.bizinikiwi_account().into();
|
||||
let data = b"hello world".to_vec();
|
||||
let value = 5_000_000_000_000u128;
|
||||
let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
|
||||
|
||||
let payload = subxt_client::apis().revive_api().instantiate(
|
||||
subxt::utils::AccountId32(origin),
|
||||
value,
|
||||
None,
|
||||
None,
|
||||
Code::Upload(bytes),
|
||||
data,
|
||||
None,
|
||||
);
|
||||
|
||||
// runtime_api.at_latest() uses the latest finalized block, query nonce accordingly
|
||||
let nonce = client
|
||||
.get_transaction_count(account.address(), BlockTag::Finalized.into())
|
||||
.await?;
|
||||
let contract_address = create1(&account.address(), nonce.try_into().unwrap());
|
||||
|
||||
let c = OnlineClient::<SrcChainConfig>::from_url("ws://localhost:45789").await?;
|
||||
let res = c.runtime_api().at_latest().await?.call(payload).await?.result.unwrap();
|
||||
|
||||
assert_eq!(res.addr, contract_address);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_invalid_transaction(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
let ethan = Account::from(subxt_signer::eth::dev::ethan());
|
||||
|
||||
let err = TransactionBuilder::new(&client)
|
||||
.value(U256::from(1_000_000_000_000u128))
|
||||
.to(ethan.address())
|
||||
.mutate(|tx| tx.chain_id = Some(42u32.into()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let call_err = unwrap_call_err!(err.source().unwrap());
|
||||
assert_eq!(call_err.message(), "Invalid Transaction");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_evm_block_from_storage(
|
||||
node_client: &OnlineClient<SrcChainConfig>,
|
||||
node_rpc_client: &RpcClient,
|
||||
block_number: U256,
|
||||
) -> anyhow::Result<Block> {
|
||||
let block_hash: H256 = node_rpc_client
|
||||
.request("chain_getBlockHash", rpc_params![block_number])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let query = subxt_client::storage().revive().ethereum_block();
|
||||
let Some(block) = node_client.storage().at(block_hash).fetch(&query).await.unwrap() else {
|
||||
return Err(anyhow!("EVM block {block_hash:?} not found"));
|
||||
};
|
||||
Ok(block.0)
|
||||
}
|
||||
|
||||
async fn test_evm_blocks_should_match(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
let (node_client, node_rpc_client, _) =
|
||||
client::connect(SharedResources::node_rpc_url()).await.unwrap();
|
||||
|
||||
// Deploy a contract to have some interesting blocks
|
||||
let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
|
||||
let value = U256::from(5_000_000_000_000u128);
|
||||
let tx = TransactionBuilder::new(&client)
|
||||
.value(value)
|
||||
.input(bytes.to_vec())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let receipt = tx.wait_for_receipt().await?;
|
||||
let block_number = receipt.block_number;
|
||||
let block_hash = receipt.block_hash;
|
||||
log::trace!(target: LOG_TARGET, "block_number = {block_number:?}");
|
||||
log::trace!(target: LOG_TARGET, "tx hash = {:?}", tx.hash());
|
||||
|
||||
let evm_block_from_storage =
|
||||
get_evm_block_from_storage(&node_client, &node_rpc_client, block_number).await?;
|
||||
|
||||
// Fetch the block immediately (should come from storage EthereumBlock)
|
||||
let evm_block_from_rpc_by_number = client
|
||||
.get_block_by_number(BlockNumberOrTag::U256(block_number.into()), false)
|
||||
.await?
|
||||
.expect("Block should exist");
|
||||
let evm_block_from_rpc_by_hash =
|
||||
client.get_block_by_hash(block_hash, false).await?.expect("Block should exist");
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
evm_block_from_rpc_by_number.transactions,
|
||||
pezpallet_revive::evm::HashesOrTransactionInfos::Hashes(_)
|
||||
),
|
||||
"Block should not have hydrated transactions"
|
||||
);
|
||||
|
||||
// All EVM blocks must match
|
||||
assert_eq!(evm_block_from_storage, evm_block_from_rpc_by_number, "EVM blocks should match");
|
||||
assert_eq!(evm_block_from_storage, evm_block_from_rpc_by_hash, "EVM blocks should match");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_evm_blocks_hydrated_should_match(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
// Deploy a contract to have some transactions in the block
|
||||
let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
|
||||
let value = U256::from(5_000_000_000_000u128);
|
||||
let signer = Account::default();
|
||||
let signer_copy = Account::default();
|
||||
let tx = TransactionBuilder::new(&client)
|
||||
.value(value)
|
||||
.signer(signer)
|
||||
.input(bytes.to_vec())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let receipt = tx.wait_for_receipt().await?;
|
||||
let block_number = receipt.block_number;
|
||||
let block_hash = receipt.block_hash;
|
||||
log::trace!(target: LOG_TARGET, "block_number = {block_number:?}");
|
||||
log::trace!(target: LOG_TARGET, "tx hash = {:?}", tx.hash());
|
||||
|
||||
// Fetch the block with hydrated transactions via RPC (by number and by hash)
|
||||
let evm_block_from_rpc_by_number = client
|
||||
.get_block_by_number(BlockNumberOrTag::U256(block_number.into()), true)
|
||||
.await?
|
||||
.expect("Block should exist");
|
||||
let evm_block_from_rpc_by_hash =
|
||||
client.get_block_by_hash(block_hash, true).await?.expect("Block should exist");
|
||||
|
||||
// Both blocks should be identical
|
||||
assert_eq!(
|
||||
evm_block_from_rpc_by_number, evm_block_from_rpc_by_hash,
|
||||
"Hydrated EVM blocks should match"
|
||||
);
|
||||
|
||||
// Verify transaction info
|
||||
let unsigned_tx = tx
|
||||
.generic_transaction()
|
||||
.try_into_unsigned()
|
||||
.expect("Transaction shall be converted");
|
||||
let signed_tx = signer_copy.sign_transaction(unsigned_tx);
|
||||
let expected_tx_info = TransactionInfo::new(&receipt, signed_tx);
|
||||
|
||||
let tx_info = if let HashesOrTransactionInfos::TransactionInfos(tx_infos) =
|
||||
evm_block_from_rpc_by_number.transactions
|
||||
{
|
||||
tx_infos[0].clone()
|
||||
} else {
|
||||
panic!("Expected hydrated transactions");
|
||||
};
|
||||
assert_eq!(expected_tx_info, tx_info, "TransationInfos should match");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_block_hash_for_tag_with_proper_ethereum_block_hash_works(
|
||||
client: Arc<WsClient>,
|
||||
) -> anyhow::Result<()> {
|
||||
// Deploy a transaction to create a block with transactions
|
||||
let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
|
||||
let value = U256::from(5_000_000_000_000u128);
|
||||
let tx = TransactionBuilder::new(&client)
|
||||
.value(value)
|
||||
.input(bytes.to_vec())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let receipt = tx.wait_for_receipt().await?;
|
||||
let ethereum_block_hash = receipt.block_hash;
|
||||
|
||||
log::trace!(target: LOG_TARGET, "Testing with Ethereum block hash: {ethereum_block_hash:?}");
|
||||
|
||||
let block_by_hash = client
|
||||
.get_block_by_hash(ethereum_block_hash, false)
|
||||
.await?
|
||||
.expect("Block should exist");
|
||||
|
||||
let account = Account::default();
|
||||
let balance = client.get_balance(account.address(), ethereum_block_hash.into()).await?;
|
||||
|
||||
assert!(balance >= U256::zero(), "Balance should be retrievable with Ethereum hash");
|
||||
assert_eq!(block_by_hash.hash, ethereum_block_hash, "Block hash should match");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_block_hash_for_tag_with_invalid_ethereum_block_hash_fails(
|
||||
client: Arc<WsClient>,
|
||||
) -> anyhow::Result<()> {
|
||||
let fake_eth_hash = H256::from([0x42u8; 32]);
|
||||
|
||||
log::trace!(target: LOG_TARGET, "Testing with fake Ethereum hash: {fake_eth_hash:?}");
|
||||
|
||||
let account = Account::default();
|
||||
let result = client.get_balance(account.address(), fake_eth_hash.into()).await;
|
||||
|
||||
assert!(result.is_err(), "Should fail with non-existent Ethereum hash");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_block_hash_for_tag_with_block_number_works(
|
||||
client: Arc<WsClient>,
|
||||
) -> anyhow::Result<()> {
|
||||
let block_number = client.block_number().await?;
|
||||
|
||||
log::trace!(target: LOG_TARGET, "Testing with block number: {block_number}");
|
||||
|
||||
let account = Account::default();
|
||||
let balance = client
|
||||
.get_balance(account.address(), BlockNumberOrTagOrHash::BlockNumber(block_number))
|
||||
.await?;
|
||||
|
||||
assert!(balance >= U256::zero(), "Balance should be retrievable with block number");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_block_hash_for_tag_with_block_tags_works(
|
||||
client: Arc<WsClient>,
|
||||
) -> anyhow::Result<()> {
|
||||
let account = Account::default();
|
||||
|
||||
let tags = vec![
|
||||
BlockTag::Latest,
|
||||
BlockTag::Finalized,
|
||||
BlockTag::Safe,
|
||||
BlockTag::Earliest,
|
||||
BlockTag::Pending,
|
||||
];
|
||||
|
||||
for tag in tags {
|
||||
let balance = client.get_balance(account.address(), tag.clone().into()).await?;
|
||||
|
||||
assert!(balance >= U256::zero(), "Balance should be retrievable with tag {tag:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_multiple_transactions_in_block(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
let num_transactions = 20;
|
||||
let alith = Account::default();
|
||||
let ethan = Account::from(subxt_signer::eth::dev::ethan());
|
||||
let amount = U256::from(1_000_000_000_000_000_000u128);
|
||||
|
||||
// Prepare EVM transfer transactions
|
||||
let transactions =
|
||||
prepare_evm_transactions(&client, alith, ethan.address(), amount, num_transactions).await?;
|
||||
|
||||
// Submit all transactions
|
||||
let submitted_txs = submit_evm_transactions(transactions).await?;
|
||||
let tx_hashes: Vec<H256> = submitted_txs.iter().map(|(hash, _, _)| *hash).collect();
|
||||
log::trace!(target: LOG_TARGET, "Submitted {} transactions", submitted_txs.len());
|
||||
|
||||
// All transactions should be included in the same block since nonces are in descending order
|
||||
let first_receipt = submitted_txs[0].2.wait_for_receipt().await?;
|
||||
|
||||
// Fetch and verify block contains all transactions
|
||||
verify_transactions_in_single_block(&client, first_receipt.block_number, &tx_hashes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_mixed_evm_bizinikiwi_transactions(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
let num_evm_txs = 10;
|
||||
let num_bizinikiwi_txs = 7;
|
||||
|
||||
let alith = Account::default();
|
||||
let ethan = Account::from(subxt_signer::eth::dev::ethan());
|
||||
let amount = U256::from(500_000_000_000_000_000u128);
|
||||
|
||||
// Prepare EVM transactions
|
||||
log::trace!(target: LOG_TARGET, "Creating {num_evm_txs} EVM transfer transactions");
|
||||
let evm_transactions =
|
||||
prepare_evm_transactions(&client, alith, ethan.address(), amount, num_evm_txs).await?;
|
||||
|
||||
// Prepare bizinikiwi transactions (simple remarks)
|
||||
log::trace!(target: LOG_TARGET, "Creating {num_bizinikiwi_txs} bizinikiwi remark transactions");
|
||||
let alice_signer = subxt_signer::sr25519::dev::alice();
|
||||
let (node_client, _, _) = client::connect(SharedResources::node_rpc_url()).await.unwrap();
|
||||
|
||||
let bizinikiwi_txs =
|
||||
prepare_bizinikiwi_transactions(&node_client, &alice_signer, num_bizinikiwi_txs).await?;
|
||||
|
||||
log::trace!(target: LOG_TARGET, "Submitting {num_evm_txs} EVM and {num_bizinikiwi_txs} bizinikiwi transactions");
|
||||
|
||||
// Submit EVM transactions
|
||||
let evm_submitted = submit_evm_transactions(evm_transactions).await?;
|
||||
let evm_tx_hashes: Vec<H256> = evm_submitted.iter().map(|(hash, _, _)| *hash).collect();
|
||||
|
||||
// Submit bizinikiwi transactions
|
||||
let bizinikiwi_futures = submit_bizinikiwi_transactions(bizinikiwi_txs).await;
|
||||
|
||||
// Wait for first EVM receipt and all bizinikiwi transactions in parallel
|
||||
let (evm_first_receipt_result, _bizinikiwi_results) = tokio::join!(
|
||||
async { evm_submitted[0].2.wait_for_receipt().await },
|
||||
futures::future::join_all(bizinikiwi_futures)
|
||||
);
|
||||
// Handle the EVM receipt result
|
||||
let evm_first_receipt = evm_first_receipt_result?;
|
||||
|
||||
// Fetch and verify block contains all transactions
|
||||
verify_transactions_in_single_block(&client, evm_first_receipt.block_number, &evm_tx_hashes)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_runtime_pallets_address_upload_code(client: Arc<WsClient>) -> anyhow::Result<()> {
|
||||
let (node_client, node_rpc_client, _) =
|
||||
client::connect(SharedResources::node_rpc_url()).await?;
|
||||
|
||||
let (bytecode, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
|
||||
let signer = Account::default();
|
||||
|
||||
// Helper function to get bizinikiwi block hash from EVM block number
|
||||
let get_bizinikiwi_block_hash = |block_number: U256| {
|
||||
let rpc_client = node_rpc_client.clone();
|
||||
async move {
|
||||
rpc_client
|
||||
.request::<pezsp_core::H256>("chain_getBlockHash", rpc_params![block_number])
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
// Step 1: Encode the Bizinikiwi upload_code call
|
||||
let upload_call = subxt::dynamic::tx(
|
||||
"Revive",
|
||||
"upload_code",
|
||||
vec![
|
||||
subxt::dynamic::Value::from_bytes(&bytecode),
|
||||
subxt::dynamic::Value::u128(u128::max_value()), // storage_deposit_limit
|
||||
],
|
||||
);
|
||||
let encoded_call = node_client.tx().call_data(&upload_call)?;
|
||||
|
||||
// Step 2: Send the encoded call to RUNTIME_PALLETS_ADDR
|
||||
let tx = TransactionBuilder::new(&client)
|
||||
.signer(signer.clone())
|
||||
.to(pezpallet_revive::RUNTIME_PALLETS_ADDR)
|
||||
.input(encoded_call.clone())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// Step 3: Wait for receipt
|
||||
let receipt = tx.wait_for_receipt().await?;
|
||||
|
||||
// Step 4: Verify transaction was successful
|
||||
assert_eq!(
|
||||
receipt.status.unwrap_or(U256::zero()),
|
||||
U256::one(),
|
||||
"Transaction should be successful"
|
||||
);
|
||||
|
||||
// Step 5: Verify the code was actually uploaded
|
||||
let code_hash = H256(pezsp_io::hashing::keccak_256(&bytecode));
|
||||
let query = subxt_client::storage().revive().pristine_code(code_hash);
|
||||
let block_hash: pezsp_core::H256 = get_bizinikiwi_block_hash(receipt.block_number).await?;
|
||||
let stored_code = node_client.storage().at(block_hash).fetch(&query).await?;
|
||||
assert!(stored_code.is_some(), "Code with hash {code_hash:?} should exist in storage");
|
||||
assert_eq!(stored_code.unwrap(), bytecode, "Stored code should match the uploaded bytecode");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user