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

Refactor cli #61

Merged
merged 3 commits into from
Dec 12, 2024
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
25 changes: 25 additions & 0 deletions crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
mod report;
mod run;
mod setup;
mod spam;
mod types;

use clap::Parser;

pub use report::report;
pub use run::run;
pub use setup::setup;
pub use spam::{spam, SpamCommandArgs};
pub use types::ContenderSubcommand;

#[derive(Parser, Debug)]
pub struct ContenderCli {
#[command(subcommand)]
pub command: ContenderSubcommand,
}

impl ContenderCli {
pub fn parse_args() -> Self {
Self::parse()
}
}
43 changes: 43 additions & 0 deletions crates/cli/src/commands/report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use contender_core::db::DbOps;
use csv::WriterBuilder;

use crate::util::write_run_txs;

pub fn report(
db: &(impl DbOps + Clone + Send + Sync + 'static),
id: Option<u64>,
out_file: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let num_runs = db.num_runs()?;
let id = if let Some(id) = id {
if id == 0 || id > num_runs {
panic!("Invalid run ID: {}", id);
}
id
} else {
if num_runs == 0 {
panic!("No runs to report");
}
// get latest run
println!("No run ID provided. Using latest run ID: {}", num_runs);
num_runs
};
let txs = db.get_run_txs(id)?;
println!("found {} txs", txs.len());
println!(
"Exporting report for run ID {:?} to out_file {:?}",
id, out_file
);

if let Some(out_file) = out_file {
let mut writer = WriterBuilder::new().has_headers(true).from_path(out_file)?;
write_run_txs(&mut writer, &txs)?;
} else {
let mut writer = WriterBuilder::new()
.has_headers(true)
.from_writer(std::io::stdout());
write_run_txs(&mut writer, &txs)?; // TODO: write a macro that lets us generalize the writer param to write_run_txs, then refactor this duplication
};

Ok(())
}
121 changes: 121 additions & 0 deletions crates/cli/src/commands/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::{env, str::FromStr, sync::Arc};

use alloy::{
eips::BlockId,
network::AnyNetwork,
providers::{Provider, ProviderBuilder},
rpc::types::BlockTransactionsKind,
transports::http::reqwest::Url,
};
use contender_core::{
agent_controller::AgentStore,
db::DbOps,
error::ContenderError,
generator::RandSeed,
spammer::{LogCallback, Spammer, TimedSpammer},
test_scenario::TestScenario,
};
use contender_testfile::TestConfig;

use crate::{
default_scenarios::{BuiltinScenario, BuiltinScenarioConfig},
util::{check_private_keys, get_signers_with_defaults, prompt_cli},
};

pub async fn run(
db: &(impl DbOps + Clone + Send + Sync + 'static),
scenario: BuiltinScenario,
rpc_url: String,
private_key: Option<String>,
interval: usize,
duration: usize,
txs_per_duration: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let user_signers = get_signers_with_defaults(private_key.map(|s| vec![s]));
let admin_signer = &user_signers[0];
let rand_seed = RandSeed::default();
let provider = ProviderBuilder::new()
.network::<AnyNetwork>()
.on_http(Url::parse(&rpc_url).expect("Invalid RPC URL"));
let block_gas_limit = provider
.get_block(BlockId::latest(), BlockTransactionsKind::Hashes)
.await?
.map(|b| b.header.gas_limit)
.ok_or(ContenderError::SetupError(
"failed getting gas limit from block",
None,
))?;

let fill_percent = env::var("C_FILL_PERCENT")
.map(|s| u16::from_str(&s).expect("invalid u16: fill_percent"))
.unwrap_or(100u16);

let scenario_config = match scenario {
BuiltinScenario::FillBlock => BuiltinScenarioConfig::fill_block(
block_gas_limit,
txs_per_duration as u64,
admin_signer.address(),
fill_percent,
),
};
let testconfig: TestConfig = scenario_config.into();
check_private_keys(&testconfig, &user_signers);

let rpc_url = Url::parse(&rpc_url).expect("Invalid RPC URL");
let mut scenario = TestScenario::new(
testconfig,
db.clone().into(),
rpc_url.to_owned(),
None,
rand_seed,
&user_signers,
AgentStore::default(),
)
.await?;

let contract_name = "SpamMe";
let contract_result = db.get_named_tx(contract_name)?;
let do_deploy_contracts = if contract_result.is_some() {
let input = prompt_cli(format!(
"{} deployment already detected. Re-deploy? [y/N]",
contract_name
));
input.to_lowercase() == "y"
} else {
true
};

if do_deploy_contracts {
println!("deploying contracts...");
scenario.deploy_contracts().await?;
}

println!("running setup...");
scenario.run_setup().await?;

let wait_duration = std::time::Duration::from_secs(interval as u64);
let spammer = TimedSpammer::new(wait_duration);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Time went backwards")
.as_millis();
let run_id = db.insert_run(timestamp as u64, duration * txs_per_duration)?;
let callback = LogCallback::new(Arc::new(
ProviderBuilder::new()
.network::<AnyNetwork>()
.on_http(rpc_url),
));

println!("starting spammer...");
spammer
.spam_rpc(
&mut scenario,
txs_per_duration,
duration,
Some(run_id),
callback.into(),
)
.await?;

Ok(())
}
64 changes: 64 additions & 0 deletions crates/cli/src/commands/setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use alloy::{
network::AnyNetwork, primitives::utils::parse_ether, providers::ProviderBuilder,
signers::local::PrivateKeySigner, transports::http::reqwest::Url,
};
use contender_core::{generator::RandSeed, test_scenario::TestScenario};
use contender_testfile::TestConfig;
use std::str::FromStr;

use crate::util::{
check_private_keys_fns, find_insufficient_balance_addrs, get_signers_with_defaults,
};

pub async fn setup(
db: &(impl contender_core::db::DbOps + Clone + Send + Sync + 'static),
testfile: impl AsRef<str>,
rpc_url: impl AsRef<str>,
private_keys: Option<Vec<String>>,
min_balance: String,
) -> Result<(), Box<dyn std::error::Error>> {
let url = Url::parse(rpc_url.as_ref()).expect("Invalid RPC URL");
let rpc_client = ProviderBuilder::new()
.network::<AnyNetwork>()
.on_http(url.to_owned());
let testconfig: TestConfig = TestConfig::from_file(testfile.as_ref())?;
let min_balance = parse_ether(&min_balance)?;

let user_signers = private_keys
.as_ref()
.unwrap_or(&vec![])
.iter()
.map(|key| PrivateKeySigner::from_str(key).expect("invalid private key"))
.collect::<Vec<PrivateKeySigner>>();
let signers = get_signers_with_defaults(private_keys);
check_private_keys_fns(
&testconfig.setup.to_owned().unwrap_or_default(),
signers.as_slice(),
);
let broke_accounts = find_insufficient_balance_addrs(
&user_signers.iter().map(|s| s.address()).collect::<Vec<_>>(),
min_balance,
&rpc_client,
)
.await?;
if !broke_accounts.is_empty() {
panic!("Some accounts do not have sufficient balance");
}

let mut scenario = TestScenario::new(
testconfig.to_owned(),
db.clone().into(),
url,
None,
RandSeed::new(),
&signers,
Default::default(),
)
.await?;

scenario.deploy_contracts().await?;
scenario.run_setup().await?;
// TODO: catch failures and prompt user to retry specific steps

Ok(())
}
Loading
Loading