diff --git a/Cargo.lock b/Cargo.lock index 53c375c..d803f09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1163,6 +1163,7 @@ dependencies = [ "contender_testfile", "csv", "serde", + "termcolor", "tokio", ] @@ -3262,6 +3263,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.63" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index eb2a826..26defc0 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -17,3 +17,4 @@ contender_testfile = { workspace = true } clap = { workspace = true, features = ["derive"] } alloy = { workspace = true, features = ["full", "node-bindings"] } csv = { workspace = true } +termcolor = "1.4.1" diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index 0687ded..3c9465d 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -1,5 +1,7 @@ use clap::{Parser, Subcommand}; +use crate::default_scenarios::BuiltinScenario; + #[derive(Parser, Debug)] pub struct ContenderCli { #[command(subcommand)] @@ -143,4 +145,45 @@ May be specified multiple times." )] out_file: Option, }, + + #[command(name = "run", long_about = "Run a builtin scenario.")] + Run { + /// The scenario to run. + scenario: BuiltinScenario, + + /// The HTTP JSON-RPC URL to target with the scenario. + rpc_url: String, + + #[arg( + short, + long = "priv-key", + long_help = "Private key used to send all transactions." + )] + private_key: Option, + + #[arg( + short, + long = "interval", + long_help = "Interval in seconds between each batch of requests.", + default_value = "12" + )] + interval: usize, + + #[arg( + short, + long = "duration", + long_help = "The number of batches of requests to send.", + default_value = "10" + )] + duration: usize, + + #[arg( + short = 'n', + long = "num-txs", + long_help = "The number of txs to send on each elapsed interval.", + default_value = "100" + )] + txs_per_duration: usize, + // TODO: DRY duplicate args + }, } diff --git a/crates/cli/src/default_scenarios/bytecode.rs b/crates/cli/src/default_scenarios/bytecode.rs new file mode 100644 index 0000000..91c4d56 --- /dev/null +++ b/crates/cli/src/default_scenarios/bytecode.rs @@ -0,0 +1 @@ +pub const SPAM_ME: &str = "0x6080604052348015600f57600080fd5b506105f98061001f6000396000f3fe60806040526004361061004a5760003560e01c806369f86ec81461004f5780639402c00414610066578063a329e8de14610086578063c5eeaf17146100a6578063fb0e722b146100ae575b600080fd5b34801561005b57600080fd5b506100646100d9565b005b34801561007257600080fd5b50610064610081366004610284565b6100e4565b34801561009257600080fd5b506100646100a136600461033d565b610119565b6100646101b1565b3480156100ba57600080fd5b506100c36101e0565b6040516100d0919061037a565b60405180910390f35b5b60325a116100da57565b6000816040516020016100f89291906103e7565b6040516020818303038152906040526000908161011591906104bb565b5050565b6000811161016d5760405162461bcd60e51b815260206004820152601a60248201527f476173206d7573742062652067726561746572207468616e2030000000000000604482015260640160405180910390fd5b6000609561017d610a288461057a565b61018791906105a1565b905080600003610195575060015b60005b818110156101ac5760008055600101610198565b505050565b60405141903480156108fc02916000818181858888f193505050501580156101dd573d6000803e3d6000fd5b50565b600080546101ed906103ad565b80601f0160208091040260200160405190810160405280929190818152602001828054610219906103ad565b80156102665780601f1061023b57610100808354040283529160200191610266565b820191906000526020600020905b81548152906001019060200180831161024957829003601f168201915b505050505081565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561029657600080fd5b813567ffffffffffffffff8111156102ad57600080fd5b8201601f810184136102be57600080fd5b803567ffffffffffffffff8111156102d8576102d861026e565b604051601f8201601f19908116603f0116810167ffffffffffffffff811182821017156103075761030761026e565b60405281815282820160200186101561031f57600080fd5b81602084016020830137600091810160200191909152949350505050565b60006020828403121561034f57600080fd5b5035919050565b60005b83811015610371578181015183820152602001610359565b50506000910152565b6020815260008251806020840152610399816040850160208701610356565b601f01601f19169190910160400192915050565b600181811c908216806103c157607f821691505b6020821081036103e157634e487b7160e01b600052602260045260246000fd5b50919050565b60008084546103f5816103ad565b60018216801561040c576001811461042157610451565b60ff1983168652811515820286019350610451565b87600052602060002060005b838110156104495781548882015260019091019060200161042d565b505081860193505b5050508351610464818360208801610356565b01949350505050565b601f8211156101ac57806000526020600020601f840160051c810160208510156104945750805b601f840160051c820191505b818110156104b457600081556001016104a0565b5050505050565b815167ffffffffffffffff8111156104d5576104d561026e565b6104e9816104e384546103ad565b8461046d565b6020601f82116001811461051d57600083156105055750848201515b600019600385901b1c1916600184901b1784556104b4565b600084815260208120601f198516915b8281101561054d578785015182556020948501946001909201910161052d565b508482101561056b5786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b8181038181111561059b57634e487b7160e01b600052601160045260246000fd5b92915050565b6000826105be57634e487b7160e01b600052601260045260246000fd5b50049056fea264697066735822122045a1a87948aab5d390113cacf93d9eb435038ea2c95e18140c4d0e3e2604afca64736f6c634300081b0033"; diff --git a/crates/cli/src/default_scenarios/mod.rs b/crates/cli/src/default_scenarios/mod.rs new file mode 100644 index 0000000..5cbedd1 --- /dev/null +++ b/crates/cli/src/default_scenarios/mod.rs @@ -0,0 +1,4 @@ +mod bytecode; +mod runconfig; + +pub use runconfig::{BuiltinScenario, BuiltinScenarioConfig}; diff --git a/crates/cli/src/default_scenarios/runconfig.rs b/crates/cli/src/default_scenarios/runconfig.rs new file mode 100644 index 0000000..476d328 --- /dev/null +++ b/crates/cli/src/default_scenarios/runconfig.rs @@ -0,0 +1,68 @@ +use alloy::primitives::Address; +use contender_core::generator::types::{CreateDefinition, FunctionCallDefinition, SpamRequest}; +use contender_testfile::TestConfig; +use serde::{Deserialize, Serialize}; + +use super::bytecode; + +#[derive(Serialize, Deserialize, Debug, Clone, clap::ValueEnum)] +pub enum BuiltinScenario { + FillBlock, +} + +pub enum BuiltinScenarioConfig { + FillBlock { + max_gas_per_block: u128, + num_txs: u64, + sender: Address, + }, +} + +impl BuiltinScenarioConfig { + pub fn fill_block(max_gas_per_block: u128, num_txs: u64, sender: Address) -> Self { + Self::FillBlock { + max_gas_per_block, + num_txs, + sender, + } + } +} + +impl From for TestConfig { + fn from(scenario: BuiltinScenarioConfig) -> Self { + match scenario { + BuiltinScenarioConfig::FillBlock { + max_gas_per_block, + num_txs, + sender, + } => { + let gas_per_tx = max_gas_per_block / num_txs as u128; + let spam_txs = (0..num_txs) + .map(|_| { + SpamRequest::Tx(FunctionCallDefinition { + to: "{SpamMe}".to_owned(), + from: Some(sender.to_string()), + signature: "consumeGas(uint256 gas)".to_owned(), + from_pool: None, + args: Some(vec![gas_per_tx.to_string()]), + value: None, + fuzz: None, + kind: Some("fill-block".to_owned()), + }) + }) + .collect::>(); + + TestConfig { + env: None, + create: Some(vec![CreateDefinition { + name: "SpamMe".to_owned(), + bytecode: bytecode::SPAM_ME.to_owned(), + from: sender.to_string(), + }]), + setup: None, + spam: Some(spam_txs), + } + } + } + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index c898a92..b6c5c1e 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,13 +1,21 @@ mod commands; +mod default_scenarios; + +use std::{ + io::Write, + str::FromStr, + sync::{Arc, LazyLock}, +}; use alloy::{ + eips::BlockId, network::{AnyNetwork, EthereumWallet, TransactionBuilder}, primitives::{ utils::{format_ether, parse_ether}, Address, U256, }, providers::{PendingTransactionConfig, Provider, ProviderBuilder}, - rpc::types::TransactionRequest, + rpc::types::{BlockTransactionsKind, TransactionRequest}, signers::local::PrivateKeySigner, transports::http::reqwest::Url, }; @@ -15,6 +23,7 @@ use commands::{ContenderCli, ContenderSubcommand}; use contender_core::{ agent_controller::{AgentStore, SignerStore}, db::{DbOps, RunTx}, + error::ContenderError, generator::{ types::{AnyProvider, EthProvider, FunctionCallDefinition, SpamRequest}, RandSeed, @@ -27,10 +36,8 @@ use contender_core::{ use contender_sqlite::SqliteDb; use contender_testfile::TestConfig; use csv::{Writer, WriterBuilder}; -use std::{ - str::FromStr, - sync::{Arc, LazyLock}, -}; +use default_scenarios::{BuiltinScenario, BuiltinScenarioConfig}; +use termcolor::{ColorChoice, ColorSpec, StandardStream, WriteColor}; static DB: LazyLock = std::sync::LazyLock::new(|| { SqliteDb::from_file("contender.db").expect("failed to open contender.db") @@ -61,7 +68,7 @@ async fn main() -> Result<(), Box> { .map(|key| PrivateKeySigner::from_str(key).expect("invalid private key")) .collect::>(); let signers = get_signers_with_defaults(private_keys); - check_private_keys( + check_private_keys_fns( &testconfig.setup.to_owned().unwrap_or_default(), signers.as_slice(), ); @@ -75,7 +82,7 @@ async fn main() -> Result<(), Box> { panic!("Some accounts do not have sufficient balance"); } - let scenario = TestScenario::new( + let mut scenario = TestScenario::new( testconfig.to_owned(), Arc::new(DB.clone()), url, @@ -121,29 +128,8 @@ async fn main() -> Result<(), Box> { .as_ref() .expect("No spam function calls found in testfile"); - // distill all FunctionCallDefinitions from the spam requests - let mut fn_calls = vec![]; // distill all from_pool arguments from the spam requests - let mut from_pools = vec![]; - - for s in spam { - match s { - SpamRequest::Tx(fn_call) => { - fn_calls.push(fn_call.to_owned()); - if let Some(from_pool) = &fn_call.from_pool { - from_pools.push(from_pool); - } - } - SpamRequest::Bundle(bundle) => { - fn_calls.extend(bundle.txs.iter().map(|s| { - if let Some(from_pool) = &s.from_pool { - from_pools.push(from_pool); - } - s.to_owned() - })); - } - } - } + let from_pools = get_from_pools(&testconfig); let mut agents = AgentStore::new(); let signers_per_period = @@ -152,7 +138,7 @@ async fn main() -> Result<(), Box> { let mut all_signers = vec![]; all_signers.extend_from_slice(&user_signers); - for from_pool in from_pools { + for from_pool in &from_pools { if agents.has_agent(from_pool) { continue; } @@ -162,57 +148,17 @@ async fn main() -> Result<(), Box> { agents.add_agent(from_pool, agent); } - check_private_keys(&fn_calls, &all_signers); + check_private_keys(&testconfig, &all_signers); - let insufficient_balance_addrs = find_insufficient_balance_addrs( - &all_signers.iter().map(|s| s.address()).collect::>(), - min_balance, + fund_accounts( &rpc_client, + ð_client, + min_balance, + &all_signers, + &user_signers[0], ) .await?; - let admin_signer = &user_signers[0]; - let mut pending_fund_txs = vec![]; - let admin_nonce = rpc_client - .get_transaction_count(admin_signer.address()) - .await?; - for (idx, address) in insufficient_balance_addrs.iter().enumerate() { - if !is_balance_sufficient(&admin_signer.address(), min_balance, &rpc_client).await? - { - // panic early if admin account runs out of funds - return Err(format!( - "Admin account {} has insufficient balance to fund this account.", - admin_signer.address() - ) - .into()); - } - - let balance = rpc_client.get_balance(*address).await?; - println!( - "Account {} has insufficient balance. (has {}, needed {})", - address, - format_ether(balance), - format_ether(min_balance) - ); - - let fund_amount = min_balance; - pending_fund_txs.push( - fund_account( - admin_signer, - *address, - fund_amount, - ð_client, - Some(admin_nonce + idx as u64), - ) - .await?, - ); - } - - for tx in pending_fund_txs { - let pending = rpc_client.watch_pending_transaction(tx).await?; - println!("funding tx confirmed ({})", pending.await?); - } - if txs_per_block.is_some() && txs_per_second.is_some() { panic!("Cannot set both --txs-per-block and --txs-per-second"); } @@ -251,7 +197,6 @@ async fn main() -> Result<(), Box> { cback.into(), ) .await?; - println!("Saved run. run_id = {}", run_id); } SpamCallbackType::Nil(cback) => { spammer @@ -278,7 +223,6 @@ async fn main() -> Result<(), Box> { spammer .spam_rpc(&mut scenario, tps, duration, Some(run_id), cback.into()) .await?; - println!("Saved run. run_id = {}", run_id); } SpamCallbackType::Nil(cback) => { spammer @@ -319,6 +263,95 @@ async fn main() -> Result<(), Box> { 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 }; } + ContenderSubcommand::Run { + scenario, + rpc_url, + private_key, + interval, + duration, + txs_per_duration, + } => { + 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::() + .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 scenario_config = match scenario { + BuiltinScenario::FillBlock => BuiltinScenarioConfig::fill_block( + block_gas_limit, + txs_per_duration as u64, + admin_signer.address(), + ), + }; + 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.clone().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::() + .on_http(rpc_url), + )); + + println!("starting spammer..."); + spammer + .spam_rpc( + &mut scenario, + txs_per_duration, + duration, + Some(run_id), + callback.into(), + ) + .await?; + } } Ok(()) } @@ -328,8 +361,51 @@ enum SpamCallbackType { Nil(NilCallback), } +fn prompt_cli(msg: impl AsRef) -> String { + let mut stdout = StandardStream::stdout(ColorChoice::Always); + stdout + .set_color(ColorSpec::new().set_fg(Some(termcolor::Color::Rgb(252, 186, 3)))) + .expect("failed to set stdout color"); + writeln!(&mut stdout, "{}", msg.as_ref()).expect("failed to write to stdout"); + stdout.reset().expect("failed to reset color"); + + let mut input = String::new(); + std::io::stdin() + .read_line(&mut input) + .expect("Failed to read line"); + input.trim().to_owned() +} + +fn check_private_keys(testconfig: &TestConfig, prv_keys: &[PrivateKeySigner]) { + let setup = testconfig.setup.to_owned().unwrap_or_default(); + let spam = testconfig + .spam + .as_ref() + .expect("No spam function calls found in testfile"); + + // distill all FunctionCallDefinitions from the spam requests + let mut fn_calls = vec![]; + + for s in setup { + fn_calls.push(s.to_owned()); + } + + for s in spam { + match s { + SpamRequest::Tx(fn_call) => { + fn_calls.push(fn_call.to_owned()); + } + SpamRequest::Bundle(bundle) => { + fn_calls.extend(bundle.txs.iter().map(|s| s.to_owned())); + } + } + } + + check_private_keys_fns(&fn_calls, prv_keys); +} + /// Panics if any of the function calls' `from` addresses do not have a corresponding private key. -fn check_private_keys(fn_calls: &[FunctionCallDefinition], prv_keys: &[PrivateKeySigner]) { +fn check_private_keys_fns(fn_calls: &[FunctionCallDefinition], prv_keys: &[PrivateKeySigner]) { for fn_call in fn_calls { if let Some(from) = &fn_call.from { let address = from.parse::
().expect("invalid 'from' address"); @@ -340,6 +416,33 @@ fn check_private_keys(fn_calls: &[FunctionCallDefinition], prv_keys: &[PrivateKe } } +fn get_from_pools(testconfig: &TestConfig) -> Vec { + let mut from_pools = vec![]; + let spam = testconfig + .spam + .as_ref() + .expect("No spam function calls found in testfile"); + + for s in spam { + match s { + SpamRequest::Tx(fn_call) => { + if let Some(from_pool) = &fn_call.from_pool { + from_pools.push(from_pool.to_owned()); + } + } + SpamRequest::Bundle(bundle) => { + for tx in &bundle.txs { + if let Some(from_pool) = &tx.from_pool { + from_pools.push(from_pool.to_owned()); + } + } + } + } + } + + from_pools +} + const DEFAULT_PRV_KEYS: [&str; 10] = [ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", @@ -373,6 +476,63 @@ fn get_signers_with_defaults(private_keys: Option>) -> Vec>() } +async fn fund_accounts( + rpc_client: &AnyProvider, + eth_client: &EthProvider, + min_balance: U256, + all_signers: &[PrivateKeySigner], + admin_signer: &PrivateKeySigner, +) -> Result<(), Box> { + let insufficient_balance_addrs = find_insufficient_balance_addrs( + &all_signers.iter().map(|s| s.address()).collect::>(), + min_balance, + rpc_client, + ) + .await?; + + let mut pending_fund_txs = vec![]; + let admin_nonce = rpc_client + .get_transaction_count(admin_signer.address()) + .await?; + for (idx, address) in insufficient_balance_addrs.iter().enumerate() { + if !is_balance_sufficient(&admin_signer.address(), min_balance, rpc_client).await? { + // panic early if admin account runs out of funds + return Err(format!( + "Admin account {} has insufficient balance to fund this account.", + admin_signer.address() + ) + .into()); + } + + let balance = rpc_client.get_balance(*address).await?; + println!( + "Account {} has insufficient balance. (has {}, needed {})", + address, + format_ether(balance), + format_ether(min_balance) + ); + + let fund_amount = min_balance; + pending_fund_txs.push( + fund_account( + admin_signer, + *address, + fund_amount, + eth_client, + Some(admin_nonce + idx as u64), + ) + .await?, + ); + } + + for tx in pending_fund_txs { + let pending = rpc_client.watch_pending_transaction(tx).await?; + println!("funding tx confirmed ({})", pending.await?); + } + + Ok(()) +} + async fn fund_account( admin_signer: &PrivateKeySigner, recipient: Address, diff --git a/crates/core/src/db/mock.rs b/crates/core/src/db/mock.rs new file mode 100644 index 0000000..dc216b2 --- /dev/null +++ b/crates/core/src/db/mock.rs @@ -0,0 +1,48 @@ +use alloy::primitives::{Address, TxHash}; + +use super::{DbOps, NamedTx, RunTx}; +use crate::Result; + +pub struct MockDb; + +impl DbOps for MockDb { + fn create_tables(&self) -> Result<()> { + Ok(()) + } + + fn insert_run(&self, _timestamp: u64, _tx_count: usize) -> Result { + Ok(0) + } + + fn num_runs(&self) -> Result { + Ok(0) + } + + fn insert_named_txs(&self, _named_txs: Vec) -> Result<()> { + Ok(()) + } + + fn get_named_tx(&self, _name: &str) -> Result> { + Ok(Some(NamedTx::new( + String::default(), + TxHash::default(), + None, + ))) + } + + fn get_named_tx_by_address(&self, address: &Address) -> Result> { + Ok(Some(NamedTx::new( + String::default(), + TxHash::default(), + Some(*address), + ))) + } + + fn insert_run_txs(&self, _run_id: u64, _run_txs: Vec) -> Result<()> { + Ok(()) + } + + fn get_run_txs(&self, _run_id: u64) -> Result> { + Ok(vec![]) + } +} diff --git a/crates/core/src/db.rs b/crates/core/src/db/mod.rs similarity index 57% rename from crates/core/src/db.rs rename to crates/core/src/db/mod.rs index 1fd19ff..349a66d 100644 --- a/crates/core/src/db.rs +++ b/crates/core/src/db/mod.rs @@ -1,8 +1,12 @@ +mod mock; + use alloy::primitives::{Address, TxHash}; use serde::Serialize; use crate::Result; +pub use mock::MockDb; + #[derive(Debug, Serialize, Clone)] pub struct RunTx { pub tx_hash: TxHash, @@ -48,51 +52,11 @@ pub trait DbOps { fn insert_named_txs(&self, named_txs: Vec) -> Result<()>; - fn get_named_tx(&self, name: &str) -> Result; + fn get_named_tx(&self, name: &str) -> Result>; - fn get_named_tx_by_address(&self, address: &Address) -> Result; + fn get_named_tx_by_address(&self, address: &Address) -> Result>; fn insert_run_txs(&self, run_id: u64, run_txs: Vec) -> Result<()>; fn get_run_txs(&self, run_id: u64) -> Result>; } - -pub struct MockDb; - -impl DbOps for MockDb { - fn create_tables(&self) -> Result<()> { - Ok(()) - } - - fn insert_run(&self, _timestamp: u64, _tx_count: usize) -> Result { - Ok(0) - } - - fn num_runs(&self) -> Result { - Ok(0) - } - - fn insert_named_txs(&self, _named_txs: Vec) -> Result<()> { - Ok(()) - } - - fn get_named_tx(&self, _name: &str) -> Result { - Ok(NamedTx::new(String::default(), TxHash::default(), None)) - } - - fn get_named_tx_by_address(&self, address: &Address) -> Result { - Ok(NamedTx::new( - String::default(), - TxHash::default(), - Some(*address), - )) - } - - fn insert_run_txs(&self, _run_id: u64, _run_txs: Vec) -> Result<()> { - Ok(()) - } - - fn get_run_txs(&self, _run_id: u64) -> Result> { - Ok(vec![]) - } -} diff --git a/crates/core/src/generator/templater.rs b/crates/core/src/generator/templater.rs index 0fadd9b..c6cd4a9 100644 --- a/crates/core/src/generator/templater.rs +++ b/crates/core/src/generator/templater.rs @@ -54,12 +54,19 @@ where continue; } - let template_value = db.get_named_tx(&template_key.to_string()).map_err(|e| { - ContenderError::SpamError( - "failed to get placeholder value from DB", - Some(format!("value={:?} ({})", template_key, e)), - ) - })?; + let template_value = db + .get_named_tx(&template_key.to_string()) + .map_err(|e| { + ContenderError::SpamError( + "failed to get placeholder value from DB", + Some(format!("value={:?} ({})", template_key, e)), + ) + })? + .ok_or(ContenderError::SpamError( + "failed to find placeholder value in DB", + Some(template_key.to_string()), + ))?; + placeholder_map.insert( template_key, template_value diff --git a/crates/core/src/spammer/spammer_trait.rs b/crates/core/src/spammer/spammer_trait.rs index c92367f..2631c68 100644 --- a/crates/core/src/spammer/spammer_trait.rs +++ b/crates/core/src/spammer/spammer_trait.rs @@ -72,7 +72,7 @@ where let mut timeout_counter = 0; if let Some(run_id) = run_id { loop { - if timeout_counter > (num_periods * txs_per_period) { + if timeout_counter > (num_periods * txs_per_period + 1) { println!("quitting due to timeout"); break; } @@ -86,6 +86,7 @@ where } timeout_counter += 1; } + println!("done spamming. run_id={}", run_id); } Ok(()) diff --git a/crates/core/src/test_scenario.rs b/crates/core/src/test_scenario.rs index a41d679..f9b6d7b 100644 --- a/crates/core/src/test_scenario.rs +++ b/crates/core/src/test_scenario.rs @@ -123,10 +123,20 @@ where }) } - pub async fn deploy_contracts( - &self, - // only_with_names: Option<&[impl AsRef]>, - ) -> Result<()> { + pub async fn sync_nonces(&mut self) -> Result<()> { + let all_addrs = self.wallet_map.keys().copied().collect::>(); + for addr in &all_addrs { + let nonce = self + .rpc_client + .get_transaction_count(*addr) + .await + .map_err(|e| ContenderError::with_err(e, "failed to retrieve nonce from RPC"))?; + self.nonces.insert(*addr, nonce); + } + Ok(()) + } + + pub async fn deploy_contracts(&mut self) -> Result<()> { let pub_provider = &self.rpc_client; let gas_price = pub_provider .get_gas_price() @@ -198,10 +208,12 @@ where })) .await?; + self.sync_nonces().await?; + Ok(()) } - pub async fn run_setup(&self) -> Result<()> { + pub async fn run_setup(&mut self) -> Result<()> { self.load_txs(PlanType::Setup(|tx_req| { /* callback */ println!("{}", self.format_setup_log(&tx_req)); @@ -260,6 +272,8 @@ where })) .await?; + self.sync_nonces().await?; + Ok(()) } @@ -518,7 +532,9 @@ where // lookup name of contract if it exists let to_name = to_address.map(|a| { let named_tx = self.db.get_named_tx_by_address(a); - named_tx.map(|t| t.name).unwrap_or_default() + named_tx + .map(|t| t.map(|tt| tt.name).unwrap_or_default()) + .unwrap_or_default() }); format!( @@ -762,7 +778,7 @@ pub mod tests { #[tokio::test] async fn scenario_creates_contracts() { let anvil = spawn_anvil(); - let scenario = get_test_scenario(&anvil).await; + let mut scenario = get_test_scenario(&anvil).await; let res = scenario.deploy_contracts().await; assert!(res.is_ok()); } @@ -770,7 +786,7 @@ pub mod tests { #[tokio::test] async fn scenario_runs_setup() { let anvil = spawn_anvil(); - let scenario = get_test_scenario(&anvil).await; + let mut scenario = get_test_scenario(&anvil).await; scenario.deploy_contracts().await.unwrap(); let res = scenario.run_setup().await; println!("{:?}", res); diff --git a/crates/sqlite_db/src/lib.rs b/crates/sqlite_db/src/lib.rs index 81cf2a3..187e53c 100644 --- a/crates/sqlite_db/src/lib.rs +++ b/crates/sqlite_db/src/lib.rs @@ -218,7 +218,7 @@ impl DbOps for SqliteDb { Ok(()) } - fn get_named_tx(&self, name: &str) -> Result { + fn get_named_tx(&self, name: &str) -> Result> { let pool = self.get_pool()?; let mut stmt = pool .prepare( @@ -232,12 +232,12 @@ impl DbOps for SqliteDb { let res = row .last() .transpose() - .map_err(|e| ContenderError::with_err(e, "no row found"))? - .ok_or(ContenderError::DbError("no existing row", None))?; - Ok(res.into()) + .map_err(|e| ContenderError::with_err(e, "failed to query row"))? + .map(|r| r.into()); + Ok(res) } - fn get_named_tx_by_address(&self, address: &Address) -> Result { + fn get_named_tx_by_address(&self, address: &Address) -> Result> { let pool = self.get_pool()?; let mut stmt = pool .prepare( @@ -249,13 +249,13 @@ impl DbOps for SqliteDb { .query_map(params![address.encode_hex()], |row| { NamedTxRow::from_row(row) }) - .map_err(|e| ContenderError::with_err(e, "failed to map row"))?; + .map_err(|e| ContenderError::with_err(e, "failed to map row query"))?; let res = row .last() .transpose() - .map_err(|e| ContenderError::with_err(e, "no row found"))? - .ok_or(ContenderError::DbError("no existing row", None))?; - Ok(res.into()) + .map_err(|e| ContenderError::with_err(e, "failed to query row"))? + .map(|r| r.into()); + Ok(res) } fn insert_run_txs(&self, run_id: u64, run_txs: Vec) -> Result<()> { @@ -342,7 +342,7 @@ mod tests { .unwrap(); assert_eq!(count, 2); - let res1 = db.get_named_tx(&name1).unwrap(); + let res1 = db.get_named_tx(&name1).unwrap().unwrap(); assert_eq!(res1.name, name1); assert_eq!(res1.tx_hash, tx_hash); assert_eq!(res1.address, contract_address);