Skip to content
Snippets Groups Projects
eth-rpc-tester.rs 5.28 KiB
Newer Older
// This file is part of Substrate.

// 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 pallet_revive::evm::{Account, BlockTag, ReceiptInfo};
use pallet_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://westend-asset-hub-rpc.polkadot.io",
			"--rpc-cors",
			"all",
			"--unsafe-rpc-external",
			"--log=sc_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, _) = pallet_revive_fixtures::compile_module("dummy")?;
	let input = bytes.into_iter().chain(data).collect::<Vec<u8>>();

	println!("Account:");
	println!("- address: {:?}", account.address());
	println!("- substrate address: {}", account.substrate_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:?}");