use anyhow::{bail, Result};
use clap::Subcommand;
use console::style;
use derive_new::new;
use serde::Deserialize;
use std::env;
use std::fmt::Formatter;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use crate::framework::{Context, Module};
use crate::support::command::run_command;
use crate::support::gas::Gas;
use super::ops::clear_admin::ClearAdminResponse;
use super::ops::execute::ExecuteResponse;
use super::ops::instantiate::InstantiateResponse;
use super::ops::migrate::MigrateResponse;
use super::ops::query::QueryResponse;
use super::ops::store_code::StoreCodeResponse;
use super::ops::update_admin::UpdateAdminResponse;
use super::{args::BaseTxArgs, config::WasmConfig, proposal::entrypoint::ProposalCmd};
use super::{ops, proposal};
#[derive(clap::ValueEnum, Clone, Debug, Deserialize)]
pub enum NodePackageManager {
Npm,
Yarn,
}
impl FromStr for NodePackageManager {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"npm" => anyhow::Ok(Self::Npm),
"yarn" => anyhow::Ok(Self::Yarn),
_ => Err(anyhow::anyhow!("must be either `npm` or `yarn`")),
}
}
}
impl From<&NodePackageManager> for String {
fn from(n: &NodePackageManager) -> Self {
match n {
NodePackageManager::Npm => "npm".to_string(),
NodePackageManager::Yarn => "yarn".to_string(),
}
}
}
impl std::fmt::Display for NodePackageManager {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(String::from(self).as_str())
}
}
#[derive(Subcommand, Debug, Deserialize)]
pub enum WasmCmd {
New {
contract_name: String,
#[clap(short, long)]
target_dir: Option<PathBuf>,
#[clap(short, long)]
version: Option<String>,
#[clap(long)]
template: Option<String>,
},
Build {
#[clap(long)]
#[serde(default = "default_value::no_wasm_opt")]
no_wasm_opt: bool,
#[clap(short, long)]
#[serde(default = "default_value::aarch64")]
aarch64: bool,
},
StoreCode {
contract_name: String,
#[clap(long)]
#[serde(default = "default_value::no_wasm_opt")]
no_wasm_opt: bool,
#[clap(long)]
permit_instantiate_only: Option<String>,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
TsGen {
contract_name: String,
#[clap(long)]
schema_gen_cmd: Option<String>,
#[clap(long)]
out_dir: Option<PathBuf>,
#[clap(long, default_value = "yarn")]
#[serde(default = "default_value::node_package_manager")]
node_package_manager: NodePackageManager,
},
UpdateAdmin {
contract_name: String,
#[clap(short, long, default_value = "default")]
#[serde(default = "default_value::label")]
label: String,
#[clap(long)]
new_admin: String,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
ClearAdmin {
contract_name: String,
#[clap(short, long, default_value = "default")]
#[serde(default = "default_value::label")]
label: String,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
Instantiate {
contract_name: String,
#[clap(short, long, default_value = "default")]
#[serde(default = "default_value::label")]
label: String,
#[clap(short, long)]
raw: Option<String>,
#[clap(long)]
admin: Option<String>,
#[clap(short, long)]
funds: Option<String>,
#[clap(long)]
#[serde(default = "default_value::no_proposal_sync")]
no_proposal_sync: bool,
#[clap(short, long)]
#[serde(default = "default_value::yes")]
yes: bool,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
Migrate {
contract_name: String,
#[clap(short, long, default_value = "default")]
#[serde(default = "default_value::label")]
label: String,
#[clap(short, long)]
raw: Option<String>,
#[clap(long)]
#[serde(default = "default_value::no_proposal_sync")]
no_proposal_sync: bool,
#[clap(short, long)]
#[serde(default = "default_value::yes")]
yes: bool,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
Deploy {
contract_name: String,
#[clap(short, long, default_value = "default")]
#[serde(default = "default_value::label")]
label: String,
#[clap(short, long)]
raw: Option<String>,
#[clap(long)]
permit_instantiate_only: Option<String>,
#[clap(long)]
admin: Option<String>,
#[clap(short, long)]
funds: Option<String>,
#[clap(long)]
#[serde(default = "default_value::no_rebuild")]
no_rebuild: bool,
#[clap(long)]
#[serde(default = "default_value::no_wasm_opt")]
no_wasm_opt: bool,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
Upgrade {
contract_name: String,
#[clap(short, long, default_value = "default")]
#[serde(default = "default_value::label")]
label: String,
#[clap(short, long)]
raw: Option<String>,
#[clap(long)]
#[serde(default = "default_value::no_rebuild")]
no_rebuild: bool,
#[clap(long)]
#[serde(default = "default_value::no_wasm_opt")]
no_wasm_opt: bool,
#[clap(long)]
permit_instantiate_only: Option<String>,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
Proposal {
#[clap(subcommand)]
cmd: ProposalCmd,
},
Execute {
contract_name: String,
#[clap(short, long, default_value = "default")]
#[serde(default = "default_value::label")]
label: String,
#[clap(short, long)]
raw: Option<String>,
#[clap(short, long)]
funds: Option<String>,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
Query {
contract_name: String,
#[clap(short, long, default_value = "default")]
#[serde(default = "default_value::label")]
label: String,
#[clap(short, long)]
raw: Option<String>,
#[clap(flatten)]
#[serde(flatten)]
base_tx_args: BaseTxArgs,
},
}
mod default_value {
use super::NodePackageManager;
pub(crate) fn label() -> String {
"default".to_string()
}
pub(crate) fn node_package_manager() -> NodePackageManager {
NodePackageManager::Yarn
}
pub(crate) fn no_wasm_opt() -> bool {
false
}
pub(crate) fn no_rebuild() -> bool {
false
}
pub(crate) fn no_proposal_sync() -> bool {
false
}
pub(crate) fn yes() -> bool {
false
}
pub(crate) fn aarch64() -> bool {
false
}
}
#[derive(new)]
pub struct WasmModule {}
impl<'a> Module<'a, WasmConfig, WasmCmd, anyhow::Error> for WasmModule {
fn execute<Ctx: Context<'a, WasmConfig>>(ctx: Ctx, cmd: &WasmCmd) -> Result<(), anyhow::Error> {
match cmd {
WasmCmd::New {
contract_name: name,
target_dir, version,
template,
} => ops::new(
&ctx,
name,
version.to_owned(),
target_dir.to_owned(),
template.to_owned(),
),
cmd @ WasmCmd::Build { .. } => build(ctx, cmd),
cmd @ WasmCmd::StoreCode { .. } => store_code(ctx, cmd).map(|_| ()),
cmd @ WasmCmd::UpdateAdmin { .. } => update_admin(ctx, cmd).map(|_| ()),
cmd @ WasmCmd::ClearAdmin { .. } => clear_admin(ctx, cmd).map(|_| ()),
cmd @ WasmCmd::Instantiate { .. } => instantiate(ctx, cmd).map(|_| ()),
cmd @ WasmCmd::Migrate { .. } => migrate(ctx, cmd).map(|_| ()),
cmd @ WasmCmd::Deploy { .. } => deploy(ctx, cmd).map(|_| ()),
cmd @ WasmCmd::Upgrade { .. } => upgrade(ctx, cmd).map(|_| ()),
WasmCmd::Proposal { cmd } => proposal::entrypoint::execute(ctx, cmd),
WasmCmd::TsGen {
contract_name,
schema_gen_cmd,
out_dir,
node_package_manager,
} => {
let root = ctx.root()?;
let sdk_path = root.join("ts/sdk");
env::set_current_dir(root.join("contracts").join(contract_name))?;
if let Some(gen) = schema_gen_cmd {
let gen = gen.replace("{contract_name}", contract_name);
let split = gen.split(' ').collect::<Vec<&str>>();
let mut command = Command::new(split.first().unwrap());
run_command(command.args(&split[1..]))?;
} else {
let mut cargo = Command::new("cargo");
run_command(cargo.arg("schema"))?;
};
if out_dir.is_some() {
println!(
" {} {}",
style("WARNING:").yellow().bold(),
style("`out_dir` is not the default location, skipping typescript bundle")
.yellow()
);
return Ok(());
}
env::set_current_dir(sdk_path)?;
let node_pkg = || Command::new(String::from(node_package_manager));
let which_node_pkg_manager =
run_command(Command::new("which").arg::<String>(node_package_manager.into()));
if which_node_pkg_manager.is_err() {
bail!("`{}` is required but missing, please install, or if you intended to use another package manager eg. `npm`, please specify different package manager via `--node-package-manager` flag", node_package_manager);
};
run_command(node_pkg().arg("install"))?;
run_command(node_pkg().arg("run").arg("codegen"))?; run_command(node_pkg().arg("run").arg("build"))?;
Ok(())
}
cmd @ WasmCmd::Execute { .. } => execute(ctx, cmd).map(|_| ()),
cmd @ WasmCmd::Query { .. } => query(ctx, cmd).map(|_| ()),
}
}
}
pub(crate) fn deploy<'a>(
ctx: impl Context<'a, WasmConfig>,
cmd: &WasmCmd,
) -> Result<InstantiateResponse> {
match cmd {
WasmCmd::Deploy {
contract_name,
label,
raw,
permit_instantiate_only,
admin,
funds,
no_rebuild,
no_wasm_opt,
base_tx_args,
} => {
let BaseTxArgs {
network,
signer_args,
gas_args,
timeout_height,
account_sequence,
}: &BaseTxArgs = base_tx_args;
ops::deploy(
&ctx,
contract_name,
label.as_str(),
raw.as_ref(),
permit_instantiate_only,
admin.as_ref(),
funds.as_ref().map(|s| s.as_str()).try_into()?,
network,
timeout_height,
{
let global_conf = ctx.global_config()?;
&Gas::from_args(
gas_args,
global_conf.gas_price(),
global_conf.gas_adjustment(),
)?
},
signer_args.private_key(&ctx.global_config()?)?,
signer_args.private_key(&ctx.global_config()?)?,
no_rebuild,
no_wasm_opt,
account_sequence,
)
}
_ => unimplemented!(),
}
}
pub(crate) fn query<'a>(ctx: impl Context<'a, WasmConfig>, cmd: &WasmCmd) -> Result<QueryResponse> {
match cmd {
WasmCmd::Query {
contract_name,
label,
raw,
base_tx_args,
} => {
let BaseTxArgs { network, .. }: &BaseTxArgs = base_tx_args;
ops::query(&ctx, contract_name, label.as_str(), raw.as_ref(), network)
}
_ => unimplemented!(),
}
}
pub(crate) fn build<'a>(ctx: impl Context<'a, WasmConfig>, cmd: &WasmCmd) -> Result<()> {
match cmd {
WasmCmd::Build {
no_wasm_opt,
aarch64,
} => ops::build(&ctx, no_wasm_opt, aarch64),
_ => unimplemented!(),
}
}
pub(crate) fn store_code<'a>(
ctx: impl Context<'a, WasmConfig>,
cmd: &WasmCmd,
) -> Result<StoreCodeResponse> {
match cmd {
WasmCmd::StoreCode {
contract_name,
no_wasm_opt,
permit_instantiate_only,
base_tx_args,
} => {
let BaseTxArgs {
network,
signer_args,
gas_args,
timeout_height,
account_sequence,
}: &BaseTxArgs = base_tx_args;
ops::store_code(
&ctx,
contract_name,
network,
no_wasm_opt,
permit_instantiate_only,
{
let global_conf = ctx.global_config()?;
&Gas::from_args(
gas_args,
global_conf.gas_price(),
global_conf.gas_adjustment(),
)?
},
timeout_height,
signer_args.private_key(&ctx.global_config()?)?,
account_sequence,
)
}
_ => unimplemented!(),
}
}
pub(crate) fn upgrade<'a>(
ctx: impl Context<'a, WasmConfig>,
cmd: &WasmCmd,
) -> Result<MigrateResponse> {
match cmd {
WasmCmd::Upgrade {
contract_name,
label,
raw,
no_rebuild,
no_wasm_opt,
permit_instantiate_only,
base_tx_args,
} => {
let BaseTxArgs {
network,
signer_args,
gas_args,
timeout_height,
account_sequence,
}: &BaseTxArgs = base_tx_args;
ops::upgrade(
&ctx,
contract_name,
label.as_str(),
raw.as_ref(),
permit_instantiate_only,
network,
timeout_height,
{
let global_conf = ctx.global_config()?;
&Gas::from_args(
gas_args,
global_conf.gas_price(),
global_conf.gas_adjustment(),
)?
},
signer_args.private_key(&ctx.global_config()?)?,
signer_args.private_key(&ctx.global_config()?)?,
no_rebuild,
no_wasm_opt,
account_sequence,
)
}
_ => unimplemented!(),
}
}
pub(crate) fn migrate<'a>(
ctx: impl Context<'a, WasmConfig>,
cmd: &WasmCmd,
) -> Result<MigrateResponse> {
match cmd {
WasmCmd::Migrate {
contract_name,
label,
raw,
no_proposal_sync,
yes,
base_tx_args,
} => {
let BaseTxArgs {
network,
signer_args,
gas_args,
timeout_height,
account_sequence,
}: &BaseTxArgs = base_tx_args;
ops::migrate(
&ctx,
contract_name,
label.as_str(),
raw.as_ref(),
*no_proposal_sync,
*yes,
network,
timeout_height,
{
let global_conf = ctx.global_config()?;
&Gas::from_args(
gas_args,
global_conf.gas_price(),
global_conf.gas_adjustment(),
)?
},
signer_args.private_key(&ctx.global_config()?)?,
account_sequence,
)
}
_ => unimplemented!(),
}
}
pub(crate) fn update_admin<'a>(
ctx: impl Context<'a, WasmConfig>,
cmd: &WasmCmd,
) -> Result<UpdateAdminResponse> {
match cmd {
WasmCmd::UpdateAdmin {
contract_name,
label,
new_admin,
base_tx_args,
} => {
let BaseTxArgs {
network,
signer_args,
gas_args,
timeout_height,
account_sequence,
}: &BaseTxArgs = base_tx_args;
ops::update_admin(
&ctx,
contract_name,
label,
network,
new_admin,
{
let global_conf = ctx.global_config()?;
&Gas::from_args(
gas_args,
global_conf.gas_price(),
global_conf.gas_adjustment(),
)?
},
timeout_height,
signer_args.private_key(&ctx.global_config()?)?,
account_sequence,
)
}
_ => unimplemented!(),
}
}
pub(crate) fn clear_admin<'a>(
ctx: impl Context<'a, WasmConfig>,
cmd: &WasmCmd,
) -> Result<ClearAdminResponse> {
match cmd {
WasmCmd::ClearAdmin {
contract_name,
label,
base_tx_args,
} => {
let BaseTxArgs {
network,
signer_args,
gas_args,
timeout_height,
account_sequence,
}: &BaseTxArgs = base_tx_args;
ops::clear_admin(
&ctx,
contract_name,
label,
network,
{
let global_conf = ctx.global_config()?;
&Gas::from_args(
gas_args,
global_conf.gas_price(),
global_conf.gas_adjustment(),
)?
},
timeout_height,
signer_args.private_key(&ctx.global_config()?)?,
account_sequence,
)
}
_ => unimplemented!(),
}
}
pub(crate) fn instantiate<'a>(
ctx: impl Context<'a, WasmConfig>,
cmd: &WasmCmd,
) -> Result<InstantiateResponse> {
match cmd {
WasmCmd::Instantiate {
contract_name,
label,
raw,
admin,
funds,
no_proposal_sync,
yes,
base_tx_args,
} => {
let BaseTxArgs {
network,
signer_args,
gas_args,
timeout_height,
account_sequence,
}: &BaseTxArgs = base_tx_args;
ops::instantiate(
&ctx,
contract_name,
label.as_str(),
raw.as_ref(),
admin.as_ref(),
*no_proposal_sync,
*yes,
funds.as_ref().map(|s| s.as_str()).try_into()?,
network,
timeout_height,
{
let global_conf = ctx.global_config()?;
&Gas::from_args(
gas_args,
global_conf.gas_price(),
global_conf.gas_adjustment(),
)?
},
signer_args.private_key(&ctx.global_config()?)?,
account_sequence,
)
}
_ => unimplemented!(),
}
}
pub(crate) fn execute<'a>(
ctx: impl Context<'a, WasmConfig>,
cmd: &WasmCmd,
) -> Result<ExecuteResponse> {
match cmd {
WasmCmd::Execute {
contract_name,
label,
raw,
funds,
base_tx_args,
} => {
let BaseTxArgs {
network,
signer_args,
gas_args,
timeout_height,
account_sequence,
}: &BaseTxArgs = base_tx_args;
ops::execute(
&ctx,
contract_name,
label.as_str(),
raw.as_ref(),
funds.as_ref().map(|s| s.as_str()).try_into()?,
network,
timeout_height,
{
let global_conf = ctx.global_config()?;
&Gas::from_args(
gas_args,
global_conf.gas_price(),
global_conf.gas_adjustment(),
)?
},
signer_args.private_key(&ctx.global_config()?)?,
account_sequence,
)
}
_ => unimplemented!(),
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, env, fs, path::Path};
use assert_fs::{prelude::*, TempDir};
use cargo_toml::{Dependency, DependencyDetail, Manifest};
use predicates::prelude::*;
use serial_test::serial;
use super::*;
struct WasmContext {}
impl<'a> Context<'a, WasmConfig> for WasmContext {}
#[test]
#[serial]
fn generate_contract_with_default_version_and_path() {
let temp = setup();
temp.child("contracts").assert(predicate::path::missing());
temp.child("contracts/counter-1")
.assert(predicate::path::missing());
temp.child("contracts/counter-2")
.assert(predicate::path::missing());
WasmModule::execute(
WasmContext {},
&WasmCmd::New {
contract_name: "counter-1".to_string(),
version: None,
target_dir: None,
template: Some("classic".into()),
},
)
.unwrap();
temp.child("contracts/counter-1")
.assert(predicate::path::exists());
env::set_current_dir(temp.to_path_buf().join(PathBuf::from("contracts"))).unwrap();
WasmModule::execute(
WasmContext {},
&WasmCmd::New {
contract_name: "counter-2".to_string(),
target_dir: None,
version: None,
template: Some("classic".into()),
},
)
.unwrap();
temp.child("contracts/counter-2")
.assert(predicate::path::exists());
temp.close().unwrap();
}
#[test]
#[serial]
fn generate_contract_with_default_version_and_path_from_child_dir() {
let temp = setup();
temp.child("contracts").assert(predicate::path::missing());
temp.child("contracts/counter-1")
.assert(predicate::path::missing());
temp.child("contracts/counter-2")
.assert(predicate::path::missing());
WasmModule::execute(
WasmContext {},
&WasmCmd::New {
contract_name: "counter-1".to_string(),
target_dir: None,
version: None,
template: Some("classic".into()),
},
)
.unwrap();
temp.child("contracts/counter-1")
.assert(predicate::path::exists());
WasmModule::execute(
WasmContext {},
&WasmCmd::New {
contract_name: "counter-2".to_string(),
target_dir: None,
version: None,
template: Some("classic".into()),
},
)
.unwrap();
temp.child("contracts/counter-2")
.assert(predicate::path::exists());
temp.close().unwrap();
}
#[test]
#[serial]
fn generate_contract_with_custom_version() {
let temp = setup();
temp.child("contracts").assert(predicate::path::missing());
temp.child("contracts/counter-1")
.assert(predicate::path::missing());
temp.child("contracts/counter-2")
.assert(predicate::path::missing());
struct WasmContext {}
impl<'a> Context<'a, WasmConfig> for WasmContext {
fn config(&self) -> Result<WasmConfig> {
let mut template_repos = HashMap::new();
template_repos.insert(
"classic".to_string(),
"https://github.com/CosmWasm/cw-template.git".to_string(),
);
Ok(WasmConfig {
template_repos,
..Default::default()
})
}
}
WasmModule::execute(
WasmContext {},
&WasmCmd::New {
contract_name: "counter-1".to_string(),
target_dir: None,
version: Some("0.16".into()),
template: Some("classic".into()),
},
)
.unwrap();
temp.child("contracts/counter-1")
.assert(predicate::path::exists());
assert_version(Path::new("contracts/counter-1/Cargo.toml"), "0.16");
WasmModule::execute(
WasmContext {},
&WasmCmd::New {
contract_name: "counter-2".to_string(),
target_dir: None,
version: Some("0.16".into()),
template: Some("classic".into()),
},
)
.unwrap();
temp.child("contracts/counter-2")
.assert(predicate::path::exists());
assert_version(Path::new("contracts/counter-2/Cargo.toml"), "0.16");
temp.close().unwrap();
}
#[test]
#[serial]
fn generate_contract_with_custom_path() {
let temp = setup();
env::set_current_dir(&temp).unwrap();
temp.child("custom-path").assert(predicate::path::missing());
temp.child("custom-path/counter-1")
.assert(predicate::path::missing());
temp.child("custom-path/counter-2")
.assert(predicate::path::missing());
WasmModule::execute(
WasmContext {},
&WasmCmd::New {
contract_name: "counter-1".to_string(),
target_dir: Some("custom-path".into()),
version: None,
template: Some("classic".into()),
},
)
.unwrap();
temp.child("custom-path/counter-1")
.assert(predicate::path::exists());
WasmModule::execute(
WasmContext {},
&WasmCmd::New {
contract_name: "counter-2".to_string(),
target_dir: Some("custom-path".into()),
version: None,
template: Some("classic".into()),
},
)
.unwrap();
temp.child("custom-path/counter-2")
.assert(predicate::path::exists());
temp.close().unwrap();
}
fn setup() -> TempDir {
let temp = assert_fs::TempDir::new().unwrap();
env::set_current_dir(&temp).unwrap();
fs::File::create("Beaker.toml").unwrap();
temp
}
fn assert_version(cargo_toml_path: &Path, expected_version: &str) {
let manifest = Manifest::from_path(cargo_toml_path).unwrap();
let version = {
if let Dependency::Detailed(DependencyDetail {
version: Some(version),
..
}) = manifest.dependencies.get("cosmwasm-std").unwrap()
{
version
} else {
""
}
};
assert!(version.starts_with(expected_version))
}
}