use chrono::Utc;
use clap::{Arg, ArgMatches};
use dotenv::dotenv;
use glob::glob;
use pfc_tester::errors::TerraRustTestingError;
use pfc_tester::{get_attribute_tx, NAME, VERSION};
use secp256k1::Secp256k1;
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use terra_rust_api::messages::wasm::{MsgInstantiateContract, MsgMigrateContract};
use terra_rust_api::{Message, PrivateKey, Terra};
use terra_rust_cli::cli_helpers;
use terra_rust_wallet::Wallet;
#[derive(Debug, Copy, Clone)]
pub struct CodeId {
code_id: u64,
stored: bool,
}
#[derive(Debug)]
pub struct Contract {
code_id: u64,
contract_address: String,
}
#[derive(Debug)]
pub struct ContractUpgrade {
code_id: CodeId,
init_file: Option<String>,
migrate_file: Option<String>,
}
fn get_wasms(resource_dir: &str) -> Result<HashMap<String, String>, TerraRustTestingError> {
let dir = Path::new(resource_dir);
if !dir.is_dir() {
return Err(TerraRustTestingError::NotADirectory(resource_dir.into()));
}
let mut wasms = HashMap::<String, String>::new();
for meta_file in glob(&format!("{}/*.wasm", resource_dir))? {
let meta = meta_file?;
if meta.metadata()?.is_file() {
if let Some(file_name) = meta.file_name() {
if let Some(base_name) = file_name.to_str().unwrap().strip_suffix(".wasm") {
wasms.insert(
String::from(base_name),
format!("{}/{}", resource_dir, base_name),
);
}
}
}
}
Ok(wasms)
}
async fn store_wasms<C: secp256k1::Signing + secp256k1::Context>(
resource_directory: &str,
secp: &Secp256k1<C>,
terra: &Terra,
key: &PrivateKey,
sleep: u64,
retries: usize,
) -> Result<HashMap<String, CodeId>, TerraRustTestingError> {
let wasms = get_wasms(resource_directory)?;
let mut codes = HashMap::<String, CodeId>::new();
let terra_wasm = terra.wasm();
for wasm_name_prefix in wasms.iter() {
let code_id = if let Ok(code_id) = env::var(format!("{}_CODE", wasm_name_prefix.0)) {
CodeId {
code_id: code_id.parse::<u64>()?,
stored: false,
}
} else {
let wasm_name = format!("{}.wasm", wasm_name_prefix.1);
let hash = terra_wasm
.store(
secp,
key,
&wasm_name,
Some(format!(
"{}/{}",
NAME.unwrap_or("PFC-TEST"),
VERSION.unwrap_or("DEV")
)),
)
.await?
.txhash;
let code_id = get_attribute_tx(
terra,
&hash,
retries,
tokio::time::Duration::from_secs(sleep),
"store_code",
"code_id",
)
.await?;
log::info!("Stored {} {}", wasm_name, code_id);
CodeId {
code_id: code_id.parse::<u64>()?,
stored: true,
}
};
codes.insert(wasm_name_prefix.0.clone(), code_id);
}
Ok(codes)
}
fn update_code_env(
filename: &str,
wasm_codes: &HashMap<String, CodeId>,
) -> Result<(), TerraRustTestingError> {
let mut file = File::create(filename)?;
let now = Utc::now();
writeln!(file, "# Generated {}", now.to_rfc3339())?;
writeln!(
file,
"# To force re-storage, remove the line. This will re-store the code, and do migrate"
)?;
writeln!(file, "#")?;
writeln!(file, "#")?;
for entry in wasm_codes.iter() {
writeln!(file, "{}_CODE={}", entry.0, entry.1.code_id)?;
}
log::info!("{} stored", filename);
Ok(())
}
async fn instantiate_migrate_contracts<'a, C: secp256k1::Signing + secp256k1::Context>(
resource_dir: &str,
secp: &Secp256k1<C>,
terra: &Terra,
key: &PrivateKey,
wasm_codes: &HashMap<String, CodeId>,
sleep: u64,
retries: usize,
wallet: Option<Wallet<'a>>,
seed: Option<&str>,
) -> Result<HashMap<String, Contract>, TerraRustTestingError> {
let mut contract_deets = HashMap::<String, Contract>::new();
let mut instantiate_messages = HashMap::<String, Message>::new();
let mut migrate_messages = HashMap::<String, Message>::new();
let account = key.public_key(secp).account()?;
let mut upgrades = HashMap::<String, ContractUpgrade>::new();
let dir = Path::new(resource_dir);
for entry in dir.read_dir()? {
let file = entry?.file_name();
let name = file.to_str().unwrap();
if name.ends_with(".migrate.json") {
let base = name.strip_suffix(".migrate.json").unwrap();
let wasms = wasm_codes
.keys()
.filter(|wasm_name| base.starts_with(&wasm_name.to_string()))
.collect::<Vec<_>>();
if wasms.len() != 1 {
return Err(TerraRustTestingError::TooManyMatches(base.to_string()));
}
let full_name = format!("{}/{}", resource_dir, name);
let wasm_name = wasms.first().unwrap().to_string();
let code_id = *wasm_codes.get(&wasm_name).unwrap();
upgrades
.entry(base.to_string())
.and_modify(|contract| contract.migrate_file = Some(full_name.clone()))
.or_insert(ContractUpgrade {
code_id, migrate_file: Some(full_name),
init_file: None,
});
} else if name.ends_with("init.json") {
let base = name.strip_suffix(".init.json").unwrap();
let wasms = wasm_codes
.keys()
.filter(|wasm_name| base.starts_with(&wasm_name.to_string()))
.collect::<Vec<_>>();
if wasms.len() != 1 {
return Err(TerraRustTestingError::TooManyMatches(base.to_string()));
}
let full_name = format!("{}/{}", resource_dir, name);
let wasm_name = wasms.first().unwrap().to_string();
let code_id = *wasm_codes.get(&wasm_name).unwrap();
upgrades
.entry(base.to_string())
.and_modify(|contract| contract.init_file = Some(full_name.clone()))
.or_insert(ContractUpgrade {
code_id,
migrate_file: None,
init_file: Some(full_name),
});
}
}
for instance in upgrades {
let instance_name = instance.0;
let deets = instance.1;
if let Ok(contract_address) = env::var(&instance_name) {
if deets.code_id.stored {
let json = cli_helpers::get_json_block_expanded(
&deets.migrate_file.unwrap(),
Some(account.clone()),
secp,
wallet.clone(),
seed,
None,
false,
)?;
let msg = MsgMigrateContract::create_from_json(
&account,
&contract_address,
deets.code_id.code_id,
&serde_json::to_string(&json)?,
)?;
migrate_messages.insert(instance_name.clone(), msg);
} else {
log::info!("Skipping {} {}", instance_name, contract_address);
}
contract_deets.insert(
instance_name,
Contract {
code_id: deets.code_id.code_id,
contract_address,
},
);
} else {
let json = cli_helpers::get_json_block_expanded(
&deets.init_file.unwrap(),
Some(account.clone()),
secp,
wallet.clone(),
seed,
None,
false,
)?;
let msg = MsgInstantiateContract::create_from_json(
&account,
Some(account.clone()),
deets.code_id.code_id,
&serde_json::to_string(&json)?,
vec![],
)?;
instantiate_messages.insert(instance_name, msg);
}
}
for m in migrate_messages {
let hash = terra
.submit_transaction_sync(
secp,
key,
vec![m.1],
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!("Contract migrated")
}
for i in instantiate_messages {
let hash = terra
.submit_transaction_sync(
secp,
key,
vec![i.1],
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?;
let codes = tx
.tx_response
.get_attribute_from_logs("instantiate_contract", "contract_address");
let contract = if let Some(code) = codes.first() {
code.1.clone()
} else {
panic!(
"{}/{} not present in TX log",
"instantiate_contract", "contract_address"
);
};
let codes = tx
.tx_response
.get_attribute_from_logs("instantiate_contract", "code_id");
let code_id = if let Some(code) = codes.first() {
code.1.clone()
} else {
panic!(
"{}/{} not present in TX log",
"instantiate_contract", "code_id"
);
};
log::info!("Contract {} instantiated", contract);
contract_deets.insert(
i.0,
Contract {
code_id: code_id.parse::<u64>()?,
contract_address: contract.to_string(),
},
);
}
Ok(contract_deets)
}
fn update_contract_env(
filename: &str,
contract_codes: &HashMap<String, Contract>,
) -> Result<(), TerraRustTestingError> {
let mut file = File::create(filename)?;
let now = Utc::now();
writeln!(file, "# Generated {}", now.to_rfc3339())?;
writeln!(
file,
"# To force re-instantiation, remove the line. This will re-instantiation the code"
)?;
writeln!(file, "#")?;
writeln!(file, "#")?;
for entry in contract_codes.iter() {
writeln!(file, "{}_CONTRACT_CODE={}", entry.0, entry.1.code_id)?;
writeln!(file, "{}={}", entry.0, entry.1.contract_address)?;
}
log::info!("{} stored", filename);
Ok(())
}
async fn run_it(matches: &ArgMatches) -> Result<(), TerraRustTestingError> {
dotenv::from_filename("code_id.env").ok();
dotenv::from_filename("contracts.env").ok();
let resource_directory = cli_helpers::get_arg_value(matches, "test-directory")?;
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_opt_from_args(matches);
let seed = cli_helpers::seed_from_args(matches);
let wasm_codes =
store_wasms(resource_directory, &secp, &terra, &from_key, sleep, retries).await?;
update_code_env("code_id.env", &wasm_codes)?;
log::info!("WASMs {:?}", wasm_codes.keys());
let contract_deets = instantiate_migrate_contracts(
resource_directory,
&secp,
&terra,
&from_key,
&wasm_codes,
sleep,
retries,
wallet,
seed,
)
.await?;
update_contract_env("contracts.env", &contract_deets)?;
Ok(())
}
async fn run() -> anyhow::Result<()> {
let app = cli_helpers::gen_cli("PFC-Loader", "pfc-loader").args(&[
Arg::new("test-directory")
.long("test-directory")
.takes_value(true)
.value_name("test-directory")
.default_value("./resources")
.help("directory where test WASM resides"),
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);
}
}