use clap::{Arg, ArgMatches};
use dotenv::dotenv;
use glob::glob;
use pfc_tester::errors::TerraRustTestingError;
use pfc_tester::errors::TerraRustTestingError::ExecResponseFail;
use pfc_tester::{NAME, VERSION};
use rand::distributions::Alphanumeric;
use rand::Rng;
use secp256k1::Secp256k1;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use terra_rust_api::bank::MsgSend;
use terra_rust_api::core_types::Coin;
use terra_rust_api::{LCDResult, MsgExecuteContract, PrivateKey, Terra};
use terra_rust_cli::cli_helpers;
use terra_rust_wallet::Wallet;
#[derive(Deserialize, Serialize, Debug)]
pub struct ExecResponse {
pub msg_index: Option<usize>,
pub event_type: Option<String>,
pub attribute_key: Option<String>,
pub attribute_value: Option<String>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ExecCommand {
pub coins: Option<String>,
pub json: serde_json::Value,
pub response: Option<Vec<ExecResponse>>,
pub error: Option<bool>,
pub error_message: Option<String>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct QueryCommand {
pub json: serde_json::Value,
pub response: serde_json::Value,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct SendCommand {
pub to: String,
pub coins: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct QueryArrayCommand {
pub json: serde_json::Value,
pub response: Vec<serde_json::Value>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum CommandType {
Exec(ExecCommand),
Query(QueryCommand),
QueryArray(QueryArrayCommand),
Send(SendCommand),
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Command {
pub sender: Option<String>,
pub contract: String,
pub comment: Option<String>,
pub to_run: CommandType,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct CommandFile {
pub random_per_file: Option<u64>,
pub random_per_command: Option<u64>,
pub commands: Vec<Command>,
}
async fn run_query<'a, C: secp256k1::Signing + secp256k1::Context>(
terra: &Terra,
account: &str,
contract: &str,
query: serde_json::Value,
response: serde_json::Value,
comment: Option<String>,
secp: &Secp256k1<C>,
wallet: Option<Wallet<'a>>,
seed: Option<&str>,
command_vars: HashMap<String, String>,
) -> Result<(), TerraRustTestingError> {
let query_str = cli_helpers::expand_block(
&serde_json::to_string(&query)?,
Some(account.into()),
secp,
wallet.clone(),
seed,
Some(command_vars.clone()),
true,
)?;
let response_str = cli_helpers::expand_block(
&serde_json::to_string(&response)?,
Some(account.into()),
secp,
wallet,
seed,
Some(command_vars),
true,
)?;
let response_returned = terra
.wasm()
.query::<LCDResult<serde_json::Value>>(contract, &query_str)
.await?
.result;
let response_returned_str = serde_json::to_string(&response_returned)?;
if response_str != response_returned_str {
Err(TerraRustTestingError::QueryResponseFail(
comment.unwrap_or_else(|| "-".into()),
contract.into(),
response_str,
response_returned_str,
))
} else {
log::info!(
"OK: Query {} {}",
comment.unwrap_or_else(|| "-".into()),
contract
);
Ok(())
}
}
async fn run_exec<'a, C: secp256k1::Signing + secp256k1::Context>(
terra: &Terra,
secp: &Secp256k1<C>,
private_key: &PrivateKey,
contract: &str,
action: serde_json::Value,
coins: Option<String>,
responses_wanted: Option<Vec<ExecResponse>>,
comment: Option<String>,
wallet: Option<Wallet<'a>>,
seed: Option<&str>,
sleep: u64,
retries: usize,
should_error: Option<bool>,
error_message: Option<String>,
command_vars: HashMap<String, String>,
) -> Result<(), TerraRustTestingError> {
let sender = private_key.public_key(secp).account()?;
let action_str = cli_helpers::expand_block(
&serde_json::to_string(&action)?,
Some(sender.clone()),
secp,
wallet.clone(),
seed,
Some(command_vars.clone()),
true,
)?;
let coin_vec = if let Some(coin_str) = coins {
Coin::parse_coins(&coin_str)?
} else {
vec![]
};
let should_fail = should_error.unwrap_or(false);
let message = MsgExecuteContract::create_from_json(&sender, contract, &action_str, &coin_vec)?;
let resp = terra
.submit_transaction_sync(
secp,
private_key,
vec![message],
Some(format!(
"{}/{}",
NAME.unwrap_or("PFC-TEST"),
VERSION.unwrap_or("DEV")
)),
)
.await;
match resp {
Ok(positive_response) => {
let hash = positive_response.txhash;
if should_fail {
return Err(TerraRustTestingError::ExecResponseShouldHaveFailed(
comment.unwrap_or_else(|| "-".into()),
contract.into(),
hash,
));
}
let tx = terra
.tx()
.get_and_wait_v1(&hash, retries, tokio::time::Duration::from_secs(sleep))
.await?;
if let Some(responses) = responses_wanted {
for response in &responses {
let mut matched = false;
let response_event_type =
&response.event_type.clone().unwrap_or_else(|| "-".into());
let response_attr_key =
&response.attribute_key.clone().unwrap_or_else(|| "-".into());
let response_attr_value = cli_helpers::expand_block(
&response
.attribute_value
.clone()
.unwrap_or_else(|| "-".into()),
Some(sender.clone()),
secp,
wallet.clone(),
seed,
Some(command_vars.clone()),
true,
)?;
if let Some(logs) = &tx.tx_response.logs {
for log_entry in logs {
if response.msg_index.is_none()
|| (log_entry.msg_index.unwrap_or(0) == response.msg_index.unwrap())
{
for event_v in &log_entry.events {
if response_event_type == "-"
|| response_event_type == &event_v.s_type
{
for attr in &event_v.attributes {
let attr_val =
attr.value.clone().unwrap_or_else(|| "-".into());
if (response_attr_key == "-"
|| response_attr_key == &attr.key)
&& (response_attr_value == "-"
|| response_attr_value.as_str() == attr_val)
{
matched = true;
log::debug!(
"event {} {} {:?}",
log_entry.msg_index.unwrap_or(0),
event_v.s_type,
event_v.attributes
);
}
}
}
}
}
}
}
if !matched {
return Err(ExecResponseFail(
comment.unwrap_or_else(|| "-".into()),
contract.into(),
response_event_type.to_string(),
response_attr_key.to_string(),
response_attr_value,
hash,
));
}
}
}
log::info!(
"OK: Exec {} {} {}",
comment.unwrap_or_else(|| "-".into()),
contract,
hash
);
}
Err(err_response) => {
if should_fail {
if let Some(error_mesage_wanted) = error_message {
if error_mesage_wanted == err_response.to_string() {
log::info!(
"OK: Exec {} {} -- Error matched",
comment.unwrap_or_else(|| "-".into()),
contract
);
} else {
return Err(TerraRustTestingError::ExecResponseShouldHaveFailedMessage(
comment.unwrap_or_else(|| "-".into()),
contract.into(),
error_mesage_wanted,
err_response.to_string(),
));
}
} else {
log::info!(
"OK: Exec {} {} -- Error",
comment.unwrap_or_else(|| "-".into()),
contract
);
log::debug!("{}", err_response.to_string())
}
} else {
return Err(err_response.into());
}
}
}
Ok(())
}
async fn run_send<'a, C: secp256k1::Signing + secp256k1::Context>(
terra: &Terra,
secp: &Secp256k1<C>,
private_key: &PrivateKey,
to: String,
coins: String,
comment: Option<String>,
wallet: Wallet<'a>,
seed: Option<&str>,
sleep: u64,
retries: usize,
command_vars: HashMap<String, String>,
) -> Result<(), TerraRustTestingError> {
let sender = private_key.public_key(secp).account()?;
let send_to = cli_helpers::expand_block(
&to,
Some(sender.clone()),
secp,
Some(wallet),
seed,
Some(command_vars),
true,
)?;
let coin_vec = Coin::parse_coins(&coins)?;
let message = MsgSend::create(sender.clone(), send_to.clone(), coin_vec)?;
let hash = terra
.submit_transaction_sync(
secp,
private_key,
vec![message],
Some(format!(
"{}/{}",
NAME.unwrap_or("PFC-TEST"),
VERSION.unwrap_or("DEV")
)),
)
.await?
.txhash;
let _tx = terra
.tx()
.get_and_wait_v1(&hash, retries, tokio::time::Duration::from_secs(sleep))
.await?;
log::info!(
"OK Bank Send {} {}-> {} # {}",
comment.unwrap_or_else(|| "-".into()),
sender,
send_to,
coins
);
Ok(())
}
async fn run_command_file<'a, C: secp256k1::Context + secp256k1::Signing>(
terra: &Terra,
secp: &Secp256k1<C>,
wallet: &Wallet<'a>,
default_key: &PrivateKey,
command_file: CommandFile,
seed: Option<&str>,
sleep: u64,
retries: usize,
) -> Result<(), TerraRustTestingError> {
let mut rng = rand::thread_rng();
let mut file_vars: HashMap<String, String> = Default::default();
if let Some(rand_per_file) = command_file.random_per_file {
for i in 1..rand_per_file + 1 {
let word: String = (0..10).map(|_| rng.sample(Alphanumeric) as char).collect();
file_vars.insert(format!("RAND_FILE_{}", i), word);
}
}
for command in command_file.commands {
let mut command_vars: HashMap<String, String> = file_vars.clone();
if let Some(rand_per_command) = command_file.random_per_command {
for i in 1..rand_per_command + 1 {
let word: String = (0..10).map(|_| rng.sample(Alphanumeric) as char).collect();
command_vars.insert(format!("RAND_CMD_{}", i), word);
}
}
let sender = if let Some(key_name) = command.sender {
wallet.get_private_key(secp, &key_name, None)?
} else {
default_key.clone()
};
match command.to_run {
CommandType::Query(query) => {
let _resp = run_query(
terra,
&sender.public_key(secp).account()?,
&command.contract,
query.json,
query.response,
command.comment,
secp,
Some(wallet.clone()),
seed,
command_vars,
)
.await?;
}
CommandType::QueryArray(_query) => {
todo!("not yet")
}
CommandType::Exec(exec) => {
let _resp = run_exec(
terra,
secp,
&sender,
&command.contract,
exec.json,
exec.coins,
exec.response,
command.comment,
Some(wallet.clone()),
seed,
sleep,
retries,
exec.error,
exec.error_message,
command_vars,
)
.await?;
}
CommandType::Send(send) => {
let _resp = run_send(
terra,
secp,
&sender,
send.to,
send.coins,
command.comment,
wallet.clone(),
seed,
sleep,
retries,
command_vars,
)
.await?;
}
}
}
Ok(())
}
async fn run_it(matches: &ArgMatches) -> Result<(), TerraRustTestingError> {
dotenv::from_filename("code_id.env").ok();
dotenv::from_filename("contracts.env").ok();
let sleep = cli_helpers::get_arg_value(matches, "sleep")?.parse::<u64>()?;
let retries = cli_helpers::get_arg_value(matches, "retries")?.parse::<usize>()?;
let secp = Secp256k1::new();
let terra = cli_helpers::lcd_from_args(matches).await?;
let from_key = cli_helpers::get_private_key(&secp, matches)?;
let wallet = cli_helpers::wallet_from_args(matches)?;
let seed = cli_helpers::seed_from_args(matches);
let glob_pattern = if let Some(dir) = matches.value_of("test-directory") {
format!("{}/*.test.json", dir)
} else if let Some(file) = matches.value_of("test-file") {
file.to_string()
} else {
panic!("Either pass in a test-directory or a test-file");
};
for test_file in glob(&glob_pattern)? {
let file = test_file?;
log::info!("{}", file.display());
let expanded_json = cli_helpers::get_json_block_expanded(
file.as_os_str().to_str().unwrap(),
None,
&secp,
Some(wallet.clone()),
seed,
None,
false,
)?;
let command_file: CommandFile = serde_json::from_value(expanded_json)?;
run_command_file(
&terra,
&secp,
&wallet,
&from_key,
command_file,
seed,
sleep,
retries,
)
.await?;
}
Ok(())
}
async fn run() -> anyhow::Result<()> {
let app = cli_helpers::gen_cli("PFC-Replayer", "pfc-replayer").args(&[
Arg::new("test-directory")
.long("test-directory")
.takes_value(true)
.value_name("test-directory")
.help("directory where test (*.test.json) JSON resides"),
Arg::new("test-file")
.long("test-file")
.takes_value(true)
.value_name("test-file")
.help("test file to replay"),
Arg::new("retries")
.long("retries")
.takes_value(true)
.value_name("retries")
.required(false)
.default_value("5")
.help("amount of times to retry fetching hash"),
Arg::new("sleep")
.long("sleep")
.takes_value(true)
.value_name("sleep")
.required(false)
.default_value("3")
.help("amount of seconds before retying to fetch hash"),
]);
Ok(run_it(&app.get_matches()).await?)
}
#[tokio::main]
async fn main() {
dotenv().ok();
env_logger::init();
if let Err(ref err) = run().await {
log::error!("{}", err);
err.chain()
.skip(1)
.for_each(|cause| log::error!("because: {}", cause));
::std::process::exit(1);
}
}