Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a cast rpc method for raw JSON-RPC reqs #2030

Merged
merged 5 commits into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ ethers-providers = { git = "https://github.com/gakonst/ethers-rs", default-featu
ethers-signers = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
eyre = "0.6.5"
rustc-hex = "2.1.0"
serde = "1.0.136"
serde_json = "1.0.67"
chrono = "0.2"
hex = "0.4.3"

[dev-dependencies]
async-trait = "0.1.53"
serde = "1.0.136"
tokio = "1.17.0"
thiserror = "1.0.30"

Expand Down
24 changes: 24 additions & 0 deletions cast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,30 @@ where
};
Ok(receipt)
}

/// Perform a raw JSON-RPC request
///
/// ```no_run
/// use cast::Cast;
/// use ethers_providers::{Provider, Http};
/// use std::convert::TryFrom;
///
/// # async fn foo() -> eyre::Result<()> {
/// let provider = Provider::<Http>::try_from("http://localhost:8545")?;
/// let cast = Cast::new(provider);
/// let result = cast.rpc("eth_getBalance", &["0xc94770007dda54cF92009BFF0dE90c06F603a09f", "latest"])
/// .await?;
/// println!("{}", result);
/// # Ok(())
/// # }
/// ```
pub async fn rpc<T>(&self, method: &str, params: T) -> Result<String>
where
T: std::fmt::Debug + serde::Serialize + Send + Sync,
{
let res = self.provider.provider().request::<T, serde_json::Value>(method, params).await?;
Ok(serde_json::to_string(&res)?)
}
}

pub struct InterfaceSource {
Expand Down
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ serde_json = "1.0.67"
regex = { version = "1.5.4", default-features = false }
rpassword = "5.0.1"
hex = "0.4.3"
itertools = "0.10.3"
serde = "1.0.133"
proptest = "1.0.0"
semver = "1.0.5"
Expand Down
1 change: 1 addition & 0 deletions cli/src/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ async fn main() -> eyre::Result<()> {
generate(shell, &mut Opts::command(), "cast", &mut std::io::stdout())
}
Subcommands::Run(cmd) => cmd.run()?,
Subcommands::Rpc(cmd) => cmd.run()?.await?,
};
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions cli/src/cmd/cast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
//! [`foundry_config::Config`].

pub mod find_block;
pub mod rpc;
pub mod run;
pub mod wallet;
76 changes: 76 additions & 0 deletions cli/src/cmd/cast/rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use crate::{cmd::Cmd, utils::consume_config_rpc_url};
use cast::Cast;
use clap::Parser;
use ethers::prelude::*;
use eyre::Result;
use futures::future::BoxFuture;
use itertools::Itertools;

#[derive(Debug, Clone, Parser)]
pub struct RpcArgs {
#[clap(short, long, env = "ETH_RPC_URL", value_name = "URL")]
rpc_url: Option<String>,
#[clap(
short = 'w',
long,
help = r#"Pass the "params" as is"#,
long_help = r#"Pass the "params" as is

If --raw is passed the first PARAM will be taken as the value of "params". If no params are given, stdin will be used. For example:

rpc eth_getBlockByNumber '["0x123", false]' --raw
=> {"method": "eth_getBlockByNumber", "params": ["0x123", false] ... }"#
)]
raw: bool,
#[clap(value_name = "METHOD", help = "RPC method name")]
method: String,
#[clap(
value_name = "PARAMS",
help = "RPC parameters",
long_help = r#"RPC parameters

Parameters are interpreted as JSON and then fall back to string. For example:

rpc eth_getBlockByNumber 0x123 false
=> {"method": "eth_getBlockByNumber", "params": ["0x123", false] ... }"#
)]
params: Vec<String>,
}

impl Cmd for RpcArgs {
type Output = BoxFuture<'static, Result<()>>;
fn run(self) -> eyre::Result<Self::Output> {
let RpcArgs { rpc_url, raw, method, params } = self;
Ok(Box::pin(Self::do_rpc(rpc_url, raw, method, params)))
}
}

impl RpcArgs {
async fn do_rpc(
rpc_url: Option<String>,
raw: bool,
method: String,
params: Vec<String>,
) -> Result<()> {
let rpc_url = consume_config_rpc_url(rpc_url);
let provider = Provider::try_from(rpc_url)?;
let params = if raw {
if params.is_empty() {
serde_json::Deserializer::from_reader(std::io::stdin())
.into_iter()
.next()
.transpose()?
.ok_or_else(|| eyre::format_err!("Empty JSON parameters"))?
} else {
Self::to_json_or_string(params.into_iter().join(" "))
}
} else {
serde_json::Value::Array(params.into_iter().map(Self::to_json_or_string).collect())
};
println!("{}", Cast::new(provider).rpc(&method, params).await?);
Ok(())
}
fn to_json_or_string(value: String) -> serde_json::Value {
serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value))
}
}
6 changes: 5 additions & 1 deletion cli/src/opts/cast.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{ClapChain, EthereumOpts};
use crate::{
cmd::cast::{find_block::FindBlockArgs, run::RunArgs, wallet::WalletSubcommands},
cmd::cast::{find_block::FindBlockArgs, rpc::RpcArgs, run::RunArgs, wallet::WalletSubcommands},
utils::{parse_ether_value, parse_u256},
};
use clap::{Parser, Subcommand, ValueHint};
Expand Down Expand Up @@ -822,6 +822,10 @@ If an address is specified, then the ABI is fetched from Etherscan."#,
about = "Runs a published transaction in a local environment and prints the trace."
)]
Run(RunArgs),
#[clap(name = "rpc")]
#[clap(visible_alias = "rp")]
#[clap(about = "Perform a raw JSON-RPC request")]
Rpc(RpcArgs),
}

pub fn parse_name_or_address(s: &str) -> eyre::Result<NameOrAddress> {
Expand Down
42 changes: 32 additions & 10 deletions cli/test-utils/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ impl TestProject {
cmd,
current_dir_lock: None,
saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
stdin_fun: None,
}
}

Expand All @@ -264,6 +265,7 @@ impl TestProject {
cmd,
current_dir_lock: None,
saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
stdin_fun: None,
}
}

Expand Down Expand Up @@ -334,7 +336,6 @@ pub fn read_string(path: impl AsRef<Path>) -> String {
}

/// A simple wrapper around a process::Command with some conveniences.
#[derive(Debug)]
pub struct TestCommand {
saved_cwd: PathBuf,
/// The project used to launch this command.
Expand All @@ -343,6 +344,7 @@ pub struct TestCommand {
cmd: Command,
// initial: Command,
current_dir_lock: Option<parking_lot::lock_api::MutexGuard<'static, parking_lot::RawMutex, ()>>,
stdin_fun: Option<Box<dyn FnOnce(process::ChildStdin)>>,
}

impl TestCommand {
Expand Down Expand Up @@ -391,6 +393,11 @@ impl TestCommand {
self
}

pub fn stdin(&mut self, fun: impl FnOnce(process::ChildStdin) + 'static) -> &mut TestCommand {
self.stdin_fun = Some(Box::new(fun));
self
}
Comment on lines +396 to +399
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is quite neat!


/// Convenience function to add `--root project.root()` argument
pub fn root_arg(&mut self) -> &mut TestCommand {
let root = self.project.root().to_path_buf();
Expand Down Expand Up @@ -449,7 +456,7 @@ impl TestCommand {

/// Returns the `stderr` of the output as `String`.
pub fn stderr_lossy(&mut self) -> String {
let output = self.cmd.output().unwrap();
let output = self.execute();
String::from_utf8_lossy(&output.stderr).to_string()
}

Expand All @@ -460,18 +467,33 @@ impl TestCommand {

/// Returns the output but does not expect that the command was successful
pub fn unchecked_output(&mut self) -> process::Output {
self.cmd.output().unwrap()
self.execute()
}

/// Gets the output of a command. If the command failed, then this panics.
pub fn output(&mut self) -> process::Output {
let output = self.cmd.output().unwrap();
let output = self.execute();
self.expect_success(output)
}

/// Executes command, applies stdin function and returns output
pub fn execute(&mut self) -> process::Output {
let mut child = self
.cmd
.stdout(process::Stdio::piped())
.stderr(process::Stdio::piped())
.stdin(process::Stdio::piped())
.spawn()
.unwrap();
if let Some(fun) = self.stdin_fun.take() {
fun(child.stdin.take().unwrap())
}
child.wait_with_output().unwrap()
}

/// Runs the command and prints its output
pub fn print_output(&mut self) {
let output = self.cmd.output().unwrap();
let output = self.execute();
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
}
Expand All @@ -482,14 +504,14 @@ impl TestCommand {
if let Some(parent) = name.parent() {
fs::create_dir_all(parent).unwrap();
}
let output = self.cmd.output().unwrap();
let output = self.execute();
fs::write(format!("{}.stdout", name.display()), &output.stdout).unwrap();
fs::write(format!("{}.stderr", name.display()), &output.stderr).unwrap();
}

/// Runs the command and asserts that it resulted in an error exit code.
pub fn assert_err(&mut self) {
let o = self.cmd.output().unwrap();
let o = self.execute();
if o.status.success() {
panic!(
"\n\n===== {:?} =====\n\
Expand All @@ -509,7 +531,7 @@ impl TestCommand {

/// Runs the command and asserts that something was printed to stderr.
pub fn assert_non_empty_stderr(&mut self) {
let o = self.cmd.output().unwrap();
let o = self.execute();
if o.status.success() || o.stderr.is_empty() {
panic!(
"\n\n===== {:?} =====\n\
Expand All @@ -529,7 +551,7 @@ impl TestCommand {

/// Runs the command and asserts that something was printed to stdout.
pub fn assert_non_empty_stdout(&mut self) {
let o = self.cmd.output().unwrap();
let o = self.execute();
if !o.status.success() || o.stdout.is_empty() {
panic!(
"\n\n===== {:?} =====\n\
Expand All @@ -549,7 +571,7 @@ impl TestCommand {

/// Runs the command and asserts that nothing was printed to stdout.
pub fn assert_empty_stdout(&mut self) {
let o = self.cmd.output().unwrap();
let o = self.execute();
if !o.status.success() || !o.stderr.is_empty() {
panic!(
"\n\n===== {:?} =====\n\
Expand Down
53 changes: 52 additions & 1 deletion cli/tests/it/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use foundry_cli_test_utils::{
util::{TestCommand, TestProject},
};
use foundry_utils::rpc::next_http_rpc_endpoint;
use std::path::PathBuf;
use std::{io::Write, path::PathBuf};

// tests that the `cast find-block` command works correctly
casttest!(finds_block, |_: TestProject, mut cmd: TestCommand| {
Expand Down Expand Up @@ -92,3 +92,54 @@ casttest!(cast_rlp, |_: TestProject, mut cmd: TestCommand| {
let out = cmd.stdout_lossy();
assert!(out.contains("[[\"0x55556666\"],[],[],[[[]]]]"), "{}", out);
});

// test for cast_rpc without arguments
casttest!(cast_rpc_no_args, |_: TestProject, mut cmd: TestCommand| {
let eth_rpc_url = next_http_rpc_endpoint();

// Call `cast rpc eth_chainId`
cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_chainId"]);
let output = cmd.stdout_lossy();
assert_eq!(output.trim_end(), r#""0x1""#);
});

// test for cast_rpc with arguments
casttest!(cast_rpc_with_args, |_: TestProject, mut cmd: TestCommand| {
let eth_rpc_url = next_http_rpc_endpoint();

// Call `cast rpc eth_getBlockByNumber 0x123 false`
cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_getBlockByNumber", "0x123", "false"]);
let output = cmd.stdout_lossy();
assert!(output.contains(r#""number":"0x123""#), "{}", output);
});

// test for cast_rpc with raw params
casttest!(cast_rpc_raw_params, |_: TestProject, mut cmd: TestCommand| {
let eth_rpc_url = next_http_rpc_endpoint();

// Call `cast rpc eth_getBlockByNumber --raw '["0x123", false]'`
cmd.args([
"rpc",
"--rpc-url",
eth_rpc_url.as_str(),
"eth_getBlockByNumber",
"--raw",
r#"["0x123", false]"#,
]);
let output = cmd.stdout_lossy();
assert!(output.contains(r#""number":"0x123""#), "{}", output);
});

// test for cast_rpc with direct params
casttest!(cast_rpc_raw_params_stdin, |_: TestProject, mut cmd: TestCommand| {
let eth_rpc_url = next_http_rpc_endpoint();

// Call `echo "\n[\n\"0x123\",\nfalse\n]\n" | cast rpc eth_getBlockByNumber --raw
cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_getBlockByNumber", "--raw"]).stdin(
|mut stdin| {
stdin.write_all(b"\n[\n\"0x123\",\nfalse\n]\n").unwrap();
},
);
let output = cmd.stdout_lossy();
assert!(output.contains(r#""number":"0x123""#), "{}", output);
});