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:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -0,0 +1,7 @@
doc
**target*
.idea/
Dockerfile
.dockerignore
.local
.env*
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n SELECT COUNT(*) as count\n FROM transaction_hashes\n WHERE block_hash = $1\n ",
"describe": {
"columns": [
{
"name": "count",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "2348bd412ca114197996e4395fd68c427245f94b80d37ec3aef04cd96fb36298"
}
@@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "\n\t\t\tSELECT block_hash, transaction_index\n\t\t\tFROM transaction_hashes\n\t\t\tWHERE transaction_hash = $1\n\t\t\t",
"describe": {
"columns": [
{
"name": "block_hash",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "transaction_index",
"ordinal": 1,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false
]
},
"hash": "29af64347f700919dc2ee12463f332be50096d4e37be04ed8b6f46ac5c242043"
}
@@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "\n\t\t SELECT transaction_index, transaction_hash\n\t\t FROM transaction_hashes\n\t\t WHERE block_hash = $1\n\t\t ",
"describe": {
"columns": [
{
"name": "transaction_index",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "transaction_hash",
"ordinal": 1,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false
]
},
"hash": "2fcbf357b3993c0065141859e5ad8c11bd7800e3e6d22e8383ab9ac8bbec25b1"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\t\t\t\t\tINSERT INTO logs(\n\t\t\t\t\t\t\tblock_hash,\n\t\t\t\t\t\t\ttransaction_index,\n\t\t\t\t\t\t\tlog_index,\n\t\t\t\t\t\t\taddress,\n\t\t\t\t\t\t\tblock_number,\n\t\t\t\t\t\t\ttransaction_hash,\n\t\t\t\t\t\t\ttopic_0, topic_1, topic_2, topic_3,\n\t\t\t\t\t\t\tdata)\n\t\t\t\t\t\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)\n\t\t\t\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
},
"hash": "3de332f45edf5ee4592bd0061956305983861ccaea79eec44518e1bca5233920"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n\t\t\tSELECT bizinikiwi_block_hash\n\t\t\tFROM eth_to_bizinikiwi_blocks\n\t\t\tWHERE ethereum_block_hash = $1\n\t\t\t",
"describe": {
"columns": [
{
"name": "bizinikiwi_block_hash",
"ordinal": 0,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "47b830cef6768ed5b119c74037482baef86a7c3d3469873a205805ef342ba031"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\t\t\t\tINSERT INTO transaction_hashes (transaction_hash, block_hash, transaction_index)\n\t\t\t\t\tVALUES ($1, $2, $3)\n\t\t\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "5c0ea8efbd2591e3ede3833acfcadf2d552140a20d84edbf90583da4619bcf2a"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n\t\t\tSELECT ethereum_block_hash\n\t\t\tFROM eth_to_bizinikiwi_blocks\n\t\t\tWHERE bizinikiwi_block_hash = $1\n\t\t\t",
"describe": {
"columns": [
{
"name": "ethereum_block_hash",
"ordinal": 0,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "7e5be81ad6f5d96bc6dbf62098cbd61d257d1ffad222317634327e12be403ab2"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n\t\t\tINSERT OR REPLACE INTO eth_to_bizinikiwi_blocks (ethereum_block_hash, bizinikiwi_block_hash)\n\t\t\tVALUES ($1, $2)\n\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "cf515b47790a2ac4b3802c29e36a07ace0c67849e5b20c92532d7a77861ebf80"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT EXISTS(SELECT 1 FROM eth_to_bizinikiwi_blocks WHERE bizinikiwi_block_hash = $1) AS \"exists!:bool\"",
"describe": {
"columns": [
{
"name": "exists!:bool",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "e712acbeb712c0a61fc2a47f966abae1ae43ffba0920a9d209d01dcfce44c5e0"
}
+82
View File
@@ -0,0 +1,82 @@
[package]
name = "pezpallet-revive-eth-rpc"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license = "Apache-2.0"
homepage.workspace = true
repository.workspace = true
description = "An Ethereum JSON-RPC server for pezpallet-revive."
default-run = "eth-rpc"
[lints]
workspace = true
[package.metadata.pezkuwi-sdk]
exclude-from-umbrella = true
[[bin]]
name = "eth-rpc"
path = "src/main.rs"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
codec = { workspace = true, features = ["derive"] }
futures = { workspace = true, features = ["thread-pool"] }
hex = { workspace = true }
jsonrpsee = { workspace = true, features = ["full"] }
log = { workspace = true }
pezpallet-revive = { workspace = true, default-features = true }
prometheus-endpoint = { workspace = true, default-features = true }
rlp = { workspace = true }
pezsc-cli = { workspace = true, default-features = true }
pezsc-rpc = { workspace = true, default-features = true }
pezsc-rpc-api = { workspace = true, default-features = true }
pezsc-service = { workspace = true, default-features = true }
serde = { workspace = true, default-features = true, features = [
"alloc",
"derive",
] }
serde_json = { workspace = true }
pezsp-arithmetic = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-crypto-hashing = { workspace = true }
pezsp-rpc = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
pezsp-timestamp = { workspace = true }
pezsp-weights = { workspace = true, default-features = true }
sqlx = { workspace = true, features = ["macros", "runtime-tokio", "sqlite"] }
subxt = { workspace = true, default-features = true, features = [
"reconnecting-rpc-client",
] }
subxt-signer = { workspace = true, features = ["unstable-eth"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"] }
[dev-dependencies]
env_logger = { workspace = true }
pezpallet-revive-fixtures = { workspace = true, default-features = true }
pretty_assertions = { workspace = true }
revive-dev-node = { workspace = true }
pezsp-io = { workspace = true, default-features = true }
[build-dependencies]
git2 = { workspace = true }
revive-dev-runtime = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
pezsp-runtime = { workspace = true, default-features = true }
[features]
runtime-benchmarks = [
"pezpallet-revive/runtime-benchmarks",
"revive-dev-node/runtime-benchmarks",
"revive-dev-runtime/runtime-benchmarks",
"pezsc-cli/runtime-benchmarks",
"pezsc-rpc-api/runtime-benchmarks",
"pezsc-rpc/runtime-benchmarks",
"pezsc-service/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-timestamp/runtime-benchmarks",
]
+62
View File
@@ -0,0 +1,62 @@
// 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 std::{fs, process::Command};
fn main() {
generate_git_revision();
generate_metadata_file();
}
fn generate_git_revision() {
let output = Command::new("rustc")
.arg("--version")
.output()
.expect("cannot get the current rustc version");
// Exports the default rustc --version output:
// e.g. rustc 1.83.0 (90b35a623 2024-11-26)
// into the usual Ethereum web3_clientVersion format
// e.g. rustc1.83.0
let rustc_version = String::from_utf8_lossy(&output.stdout)
.split_whitespace()
.take(2)
.collect::<Vec<_>>()
.join("");
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_string());
let (branch, id) = if let Ok(repo) = git2::Repository::open("../../../..") {
let head = repo.head().expect("should have head");
let commit = head.peel_to_commit().expect("should have commit");
let branch = head.shorthand().unwrap_or("unknown").to_string();
let id = &commit.id().to_string()[..7];
(branch, id.to_string())
} else {
("unknown".to_string(), "unknown".to_string())
};
println!("cargo:rustc-env=RUSTC_VERSION={rustc_version}");
println!("cargo:rustc-env=TARGET={target}");
println!("cargo:rustc-env=GIT_REVISION={branch}-{id}");
}
fn generate_metadata_file() {
let mut ext = pezsp_io::TestExternalities::new(Default::default());
ext.execute_with(|| {
let metadata = revive_dev_runtime::Runtime::metadata_at_version(16).unwrap();
let bytes: &[u8] = &metadata;
fs::write("revive_chain.scale", bytes).unwrap();
});
}
@@ -0,0 +1,32 @@
FROM docker.io/paritytech/ci-unified:bullseye-1.88.0-2025-06-27-v202511141243 AS builder
WORKDIR /pezkuwi
COPY . /pezkuwi
# Install build dependencies for wasm-opt-sys (binutils provides 'ar')
RUN apt-get update && apt-get install -y --no-install-recommends \
binutils \
build-essential \
cmake \
&& rm -rf /var/lib/apt/lists/*
RUN cargo fetch
RUN cargo build --locked --profile production -p pallet-revive-eth-rpc --bin eth-rpc
FROM docker.io/parity/base-bin:latest
COPY --from=builder /pezkuwi/target/production/eth-rpc /usr/local/bin
USER root
RUN useradd -m -u 1001 -U -s /bin/sh -d /pezkuwi pezkuwi && \
rm -rf /usr/bin /usr/sbin && \
/usr/local/bin/eth-rpc --help
USER pezkuwi
# 8545 is the default port for the RPC server
# 9616 is the default port for the prometheus metrics
EXPOSE 8545 9616
ENTRYPOINT ["/usr/local/bin/eth-rpc"]
# We call the help by default
CMD ["--help"]
@@ -0,0 +1,48 @@
## Start the node
Start the kitchensink node:
```bash
RUST_LOG="error,evm=debug,sc_rpc_server=info,runtime::revive=debug" cargo run --bin bizinikiwi-node -- --dev
```
## Start a zombienet network
Alternatively, you can start a zombienet network with the zagros Asset Hub teyrchain:
Prerequisites for running a local network:
- download latest [zombienet release](https://github.com/paritytech/zombienet/releases);
- build PezkuwiChain binary by running `cargo build -p pezkuwi --release --features fast-runtime` command in the
[`pezkuwi-sdk`](https://github.com/pezkuwichain/pezkuwi-sdk) repository clone;
- build PezkuwiChain Teyrchain binary by running `cargo build -p pezkuwi-teyrchain-bin --release` command in the
[`pezkuwi-sdk`](https://github.com/pezkuwichain/pezkuwi-sdk) repository clone;
```bash
zombienet spawn --provider native zagros_local_network.toml
```
## Start the RPC server
This command starts the Ethereum JSON-RPC server, which runs on `localhost:8545` by default:
```bash
RUST_LOG="info,eth-rpc=debug" cargo run -p pezpallet-revive-eth-rpc -- --dev
```
## Rust examples
Run one of the examples from the `examples` directory to send a transaction to the node:
```bash
RUST_LOG="info,eth-rpc=debug" cargo run -p pezpallet-revive-eth-rpc --example deploy
```
## JS examples
JS examples have been moved to the [evm-test-suite](https://github.com/paritytech/evm-test-suite) repository.
### Configure MetaMask
See the doc [here](https://contracts.polkadot.io/work-with-a-local-node#metemask-configuration) for more
information on how to configure MetaMask.
@@ -0,0 +1,82 @@
// 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 jsonrpsee::http_client::HttpClientBuilder;
use pezpallet_revive::{
create1,
evm::{Account, BlockTag, ReceiptInfo, U256},
};
use pezpallet_revive_eth_rpc::{example::TransactionBuilder, EthRpcClient};
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
let account = Account::default();
let data = vec![];
let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
let input = bytes.into_iter().chain(data.clone()).collect::<Vec<u8>>();
println!("Account:");
println!("- address: {:?}", account.address());
println!("- bizinikiwi: {}", account.bizinikiwi_account());
let client = Arc::new(HttpClientBuilder::default().build("http://localhost:8545")?);
println!("\n\n=== Deploying contract ===\n\n");
let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?;
let tx = TransactionBuilder::new(&client)
.value(5_000_000_000_000u128.into())
.input(input)
.send()
.await?;
println!("Deploy Tx hash: {:?}", tx.hash());
let ReceiptInfo { block_number, gas_used, contract_address, .. } =
tx.wait_for_receipt().await?;
let contract_address = contract_address.unwrap();
assert_eq!(contract_address, create1(&account.address(), nonce.try_into().unwrap()));
println!("Receipt:");
println!("- Block number: {block_number}");
println!("- Gas estimated: {}", tx.gas());
println!("- Gas used: {gas_used}");
println!("- Contract address: {contract_address:?}");
let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?;
println!("- Contract balance: {balance:?}");
if std::env::var("SKIP_CALL").is_ok() {
return Ok(());
}
println!("\n\n=== Calling contract ===\n\n");
let tx = TransactionBuilder::new(&client)
.value(U256::from(1_000_000u32))
.to(contract_address)
.send()
.await?;
println!("Contract call tx hash: {:?}", tx.hash());
let ReceiptInfo { block_number, gas_used, to, .. } = tx.wait_for_receipt().await?;
println!("Receipt:");
println!("- Block number: {block_number}");
println!("- Gas used: {gas_used}");
println!("- Gas estimated: {}", tx.gas());
println!("- To: {to:?}");
Ok(())
}
@@ -0,0 +1,171 @@
// 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 clap::Parser;
use jsonrpsee::http_client::HttpClientBuilder;
use pezpallet_revive::evm::{Account, BlockTag, ReceiptInfo};
use pezpallet_revive_eth_rpc::{example::TransactionBuilder, EthRpcClient};
use std::sync::Arc;
use tokio::{
io::{AsyncBufReadExt, BufReader},
process::{Child, ChildStderr, Command},
signal::unix::{signal, SignalKind},
};
const DOCKER_CONTAINER_NAME: &str = "eth-rpc-test";
#[derive(Parser, Debug)]
#[clap(author, about, version)]
pub struct CliCommand {
/// The eth-rpc url to connect to
#[clap(long, default_value = "http://127.0.0.1:8545")]
pub rpc_url: String,
/// The parity docker image e.g eth-rpc:master-fb2e414f
/// When not specified, no eth-rpc docker image is started
/// and the test runs against the provided `rpc_url` directly.
#[clap(long)]
docker_image: Option<String>,
/// The docker binary
/// Either docker or podman
#[clap(long, default_value = "docker")]
docker_bin: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let CliCommand { docker_bin, rpc_url, docker_image, .. } = CliCommand::parse();
let Some(docker_image) = docker_image else {
println!("Docker image not specified, using: {rpc_url:?}");
return test_eth_rpc(&rpc_url).await;
};
let mut docker_process = start_docker(&docker_bin, &docker_image)?;
let stderr = docker_process.stderr.take().unwrap();
tokio::select! {
result = docker_process.wait() => {
println!("docker failed: {result:?}");
}
_ = interrupt() => {
kill_docker().await?;
}
_ = wait_and_test_eth_rpc(stderr, &rpc_url) => {
kill_docker().await?;
}
}
Ok(())
}
async fn interrupt() {
let mut sigint = signal(SignalKind::interrupt()).expect("failed to listen for SIGINT");
let mut sigterm = signal(SignalKind::terminate()).expect("failed to listen for SIGTERM");
tokio::select! {
_ = sigint.recv() => {},
_ = sigterm.recv() => {},
}
}
fn start_docker(docker_bin: &str, docker_image: &str) -> anyhow::Result<Child> {
let docker_process = Command::new(docker_bin)
.args([
"run",
"--name",
DOCKER_CONTAINER_NAME,
"--rm",
"-p",
"8545:8545",
&format!("docker.io/paritypr/{docker_image}"),
"--node-rpc-url",
"wss://zagros-asset-hub-rpc.pezkuwichain.io",
"--rpc-cors",
"all",
"--unsafe-rpc-external",
"--log=pezsc_rpc_server:info",
])
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
Ok(docker_process)
}
async fn kill_docker() -> anyhow::Result<()> {
Command::new("docker").args(["kill", DOCKER_CONTAINER_NAME]).output().await?;
Ok(())
}
async fn wait_and_test_eth_rpc(stderr: ChildStderr, rpc_url: &str) -> anyhow::Result<()> {
let mut reader = BufReader::new(stderr).lines();
while let Some(line) = reader.next_line().await? {
println!("{line}");
if line.contains("Running JSON-RPC server") {
break;
}
}
test_eth_rpc(rpc_url).await
}
async fn test_eth_rpc(rpc_url: &str) -> anyhow::Result<()> {
let account = Account::default();
let data = vec![];
let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
let input = bytes.into_iter().chain(data).collect::<Vec<u8>>();
println!("Account:");
println!("- address: {:?}", account.address());
println!("- bizinikiwi address: {}", account.bizinikiwi_account());
let client = Arc::new(HttpClientBuilder::default().build(rpc_url)?);
let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?;
let balance = client.get_balance(account.address(), BlockTag::Latest.into()).await?;
println!("- nonce: {nonce:?}");
println!("- balance: {balance:?}");
println!("\n\n=== Deploying dummy contract ===\n\n");
let tx = TransactionBuilder::new(&client).input(input).send().await?;
println!("Hash: {:?}", tx.hash());
println!("Waiting for receipt...");
let ReceiptInfo { block_number, gas_used, contract_address, .. } =
tx.wait_for_receipt().await?;
let contract_address = contract_address.unwrap();
println!("\nReceipt:");
println!("Block explorer: https://westend-asset-hub-eth-explorer.parity.io/{:?}", tx.hash());
println!("- Block number: {block_number}");
println!("- Gas used: {gas_used}");
println!("- Address: {contract_address:?}");
println!("\n\n=== Calling dummy contract ===\n\n");
let tx = TransactionBuilder::new(&client).to(contract_address).send().await?;
println!("Hash: {:?}", tx.hash());
println!("Waiting for receipt...");
let ReceiptInfo { block_number, gas_used, to, .. } = tx.wait_for_receipt().await?;
println!("\nReceipt:");
println!("Block explorer: https://westend-asset-hub-eth-explorer.parity.io/{:?}", tx.hash());
println!("- Block number: {block_number}");
println!("- Gas used: {gas_used}");
println!("- To: {to:?}");
Ok(())
}
@@ -0,0 +1,54 @@
// 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 pezpallet_revive_eth_rpc::subxt_client::{
self, revive::calls::types::InstantiateWithCode, SrcChainConfig,
};
use pezsp_weights::Weight;
use subxt::OnlineClient;
use subxt_signer::sr25519::dev;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = OnlineClient::<SrcChainConfig>::new().await?;
let (bytes, _) = pezpallet_revive_fixtures::compile_module("dummy")?;
let tx_payload = subxt_client::tx().revive().instantiate_with_code(
0u32.into(),
Weight::from_parts(100_000, 0).into(),
3_000_000_000_000_000_000,
bytes,
vec![],
None,
);
let res = client
.tx()
.sign_and_submit_then_watch_default(&tx_payload, &dev::alice())
.await?
.wait_for_finalized()
.await?;
println!("Transaction finalized: {:?}", res.extrinsic_hash());
let block_hash = res.block_hash();
let block = client.blocks().at(block_hash).await.unwrap();
let extrinsics = block.extrinsics().await.unwrap();
extrinsics.find_first::<InstantiateWithCode>()?;
Ok(())
}
@@ -0,0 +1,43 @@
// 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 pezpallet_revive_eth_rpc::subxt_client::{self, system::calls::types::Remark, SrcChainConfig};
use subxt::OnlineClient;
use subxt_signer::sr25519::dev;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = OnlineClient::<SrcChainConfig>::new().await?;
let tx_payload = subxt_client::tx().system().remark(b"bonjour".to_vec());
let res = client
.tx()
.sign_and_submit_then_watch_default(&tx_payload, &dev::alice())
.await?
.wait_for_finalized()
.await?;
println!("Transaction finalized: {:?}", res.extrinsic_hash());
let block_hash = res.block_hash();
let block = client.blocks().at(block_hash).await.unwrap();
let extrinsics = block.extrinsics().await.unwrap();
let remarks = extrinsics
.find::<Remark>()
.map(|remark| remark.unwrap().value)
.collect::<Vec<_>>();
dbg!(remarks);
Ok(())
}
@@ -0,0 +1,42 @@
// 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 jsonrpsee::http_client::HttpClientBuilder;
use pezpallet_revive::evm::{Account, BlockTag};
use pezpallet_revive_eth_rpc::EthRpcClient;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let account = Account::default();
println!("Account address: {:?}", account.address());
let client = Arc::new(HttpClientBuilder::default().build("http://localhost:8545")?);
let block = client.get_block_by_number(BlockTag::Latest.into(), false).await?;
println!("Latest block: {block:#?}");
let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?;
println!("Account nonce: {nonce:?}");
let balance = client.get_balance(account.address(), BlockTag::Latest.into()).await?;
println!("Account balance: {balance:?}");
let sync_state = client.syncing().await?;
println!("Sync state: {sync_state:?}");
Ok(())
}
@@ -0,0 +1,58 @@
// 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 jsonrpsee::http_client::HttpClientBuilder;
use pezpallet_revive::evm::{Account, BlockTag, ReceiptInfo};
use pezpallet_revive_eth_rpc::{example::TransactionBuilder, EthRpcClient};
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Arc::new(HttpClientBuilder::default().build("http://localhost:8545")?);
let alith = Account::default();
let alith_address = alith.address();
let ethan = Account::from(subxt_signer::eth::dev::ethan());
let value = 1_000_000_000_000_000_000_000u128.into();
let print_balance = || async {
let balance = client.get_balance(alith_address, BlockTag::Latest.into()).await?;
println!("Alith {alith_address:?} balance: {balance:?}");
let balance = client.get_balance(ethan.address(), BlockTag::Latest.into()).await?;
println!("ethan {:?} balance: {balance:?}", ethan.address());
anyhow::Result::<()>::Ok(())
};
print_balance().await?;
println!("\n\n=== Transferring ===\n\n");
let tx = TransactionBuilder::new(&client)
.signer(alith)
.value(value)
.to(ethan.address())
.send()
.await?;
println!("Transaction hash: {:?}", tx.hash());
let ReceiptInfo { block_number, gas_used, status, .. } = tx.wait_for_receipt().await?;
println!("Receipt: ");
println!("- Block number: {block_number}");
println!("- Gas used: {gas_used}");
println!("- Success: {status:?}");
print_balance().await?;
Ok(())
}
@@ -0,0 +1,37 @@
[settings]
node_spawn_timeout = 240
[relaychain]
default_command = "{{PEZKUWI_BINARY}}"
default_args = ["-lteyrchain=debug,xcm=trace"]
chain = "zagros-local"
[[relaychain.nodes]]
name = "alice-zagros-validator"
validator = true
rpc_port = 9935
ws_port = 9945
balance = 2000000000000
[[relaychain.nodes]]
name = "bob-zagros-validator"
validator = true
rpc_port = 9936
ws_port = 9946
balance = 2000000000000
[[teyrchains]]
id = 1000
chain = "asset-hub-zagros-local"
cumulus_based = true
[[teyrchains.collators]]
name = "asset-hub-zagros-collator1"
rpc_port = 9011
ws_port = 9944
command = "{{PEZKUWI_TEYRCHAIN_BINARY}}"
args = ["-lteyrchain=debug,runtime::revive=debug"]
[[teyrchains.collators]]
name = "asset-hub-zagros-collator2"
command = "{{PEZKUWI_TEYRCHAIN_BINARY}}"
args = ["-lteyrchain=debug,runtime::revive=debug"]
@@ -0,0 +1,22 @@
-- Useful commands:
--
-- Set DATABASE_URL environment variable.
-- export DATABASE_URL=sqlite:///$HOME/eth_rpc.db
--
-- Create DB:
-- cargo sqlx database create
--
-- Run migration manually:
-- cargo sqlx migrate run
--
-- Update compile time artifacts:
-- cargo sqlx prepare
CREATE TABLE IF NOT EXISTS transaction_hashes (
transaction_hash BLOB NOT NULL PRIMARY KEY,
transaction_index INTEGER NOT NULL,
block_hash BLOB NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_block_hash ON transaction_hashes (
block_hash
);
@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS logs (
block_hash BLOB NOT NULL,
transaction_index INTEGER NOT NULL,
log_index INTEGER NOT NULL,
address BLOB NOT NULL,
block_number INTEGER NOT NULL,
transaction_hash BLOB NOT NULL,
topic_0 BLOB,
topic_1 BLOB,
topic_2 BLOB,
topic_3 BLOB,
data BLOB,
PRIMARY KEY (block_hash, transaction_index, log_index)
);
CREATE INDEX IF NOT EXISTS idx_block_number_address_topics ON logs (
block_number,
address,
topic_0,
topic_1,
topic_2,
topic_3
);
CREATE INDEX IF NOT EXISTS idx_block_hash ON logs (
block_hash
);
@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS eth_to_substrate_blocks (
ethereum_block_hash BLOB NOT NULL PRIMARY KEY,
substrate_block_hash BLOB NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_substrate_block_hash ON eth_to_substrate_blocks (
substrate_block_hash
);
@@ -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)
}
}
}
+276
View File
@@ -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) = &params.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, &ethereum_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);
}
+451
View File
@@ -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::*;
+787
View File
@@ -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(())
}