From 34eddea1a5a83677448ff181840127fad058ca9f Mon Sep 17 00:00:00 2001 From: NikitaM Date: Tue, 19 May 2020 16:18:38 +0300 Subject: [PATCH] Add callex cmd with user-friendly params (#16) - Function parameters must be defined in the form: --name value. Without quotes. - Integer parameter can be suffixed with `T`. Such parameter is automatically converted to nanovalue. - Array of integers can be defined without brackets `[]`. - [ADDRESS] [ABI] and [SIGN] positional parameters of `callex` commands can be omitted and in that case default values will be used from config file. --- Cargo.toml | 3 +- README.md | 21 +++++++++++++ src/call.rs | 65 ++++++++++++++++++++++++++++++++++++++ src/convert.rs | 22 +++++++++++++ src/main.rs | 84 +++++++++++++++++++++++++++++++++++++------------- tests/cli.rs | 60 ++++++++++++++++++++++++++++++++++++ 6 files changed, 233 insertions(+), 22 deletions(-) create mode 100644 src/convert.rs diff --git a/Cargo.toml b/Cargo.toml index 786e25d8..9906d867 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" license-file = "LICENSE.md" keywords = ["TON", "SDK", "smart contract", "tonlabs"] edition = "2018" -version = "0.1.2" +version = "0.1.3" [dependencies] base64 = "0.10.1" @@ -24,6 +24,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_derive = "1.0.91" +ton_abi = { git = "https://github.com/tonlabs/ton-labs-abi.git" } ton-client-rs = { git = 'https://github.com/tonlabs/ton-client-rs.git', default-features = false, branch = "0.23.0-rc" } ton_client = { git = 'https://github.com/tonlabs/TON-SDK.git', default_features = false, features = ["node_interaction"] } ton_sdk = { git = 'https://github.com/tonlabs/TON-SDK.git', default-features = false } diff --git a/README.md b/README.md index 8767d371..81f41680 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,27 @@ If `--abi` or `--sign` option is omitted in parameters it must present in config tonos-cli call [--abi ] [--sign ]
+If `--abi` or `--sign` option is omitted in parameters, it must be specified in the config file. See below for more details. + +Alternative command: + + tonos-cli callex [
] [] [] params... + +`params...` - one or more function arguments in the form of `--name value`. + +`address`, `abi`, and `keys` parameters can be omitted. In this case default values will be used from config file. + +Example: + + tonos-cli callex submitTransaction 0:1b91c010f35b1f5b42a05ad98eb2df80c302c37df69651e1f5ac9c69b7e90d4e SafeMultisigWallet.abi.json msig.keys.json --dest 0:c63a050fe333fac24750e90e4c6056c477a2526f6217b5b519853c30495882c9 --value 1.5T --bounce false --allBalance false --payload "" + +Integer and address types can be supplied without quotes. + +`--value 1.5T` - suffix `T` converts integer to nanotokens -> `1500000000`. The same as `--value 1500000000`. + +Arrays can be used without `[]` brackets. + + Run get-method: tonos-cli run [--abi ]
diff --git a/src/call.rs b/src/call.rs index 0a0f853a..850c5652 100644 --- a/src/call.rs +++ b/src/call.rs @@ -13,6 +13,8 @@ */ use crate::config::Config; use crate::crypto::load_keypair; +use crate::convert; +use ton_abi::{Contract, ParamType}; use chrono::{TimeZone, Local}; use hex; use std::time::SystemTime; @@ -267,4 +269,67 @@ pub fn call_contract_with_msg(conf: Config, str_msg: String, abi: String) -> Res println!("Result: {}", serde_json::to_string_pretty(&result.output).unwrap()); } Ok(()) +} + +fn parse_integer_param(value: &str) -> Result { + let value = value.trim_matches('\"'); + + if value.ends_with('T') { + convert::convert_token(value.trim_end_matches('T')) + } else { + Ok(value.to_owned()) + } +} + +fn build_json_from_params(params_vec: Vec<&str>, abi: &str, method: &str) -> Result { + let abi_obj = Contract::load(abi.as_bytes()).map_err(|e| format!("failed to parse ABI: {}", e))?; + let functions = abi_obj.functions(); + + let func_obj = functions.get(method).unwrap(); + let inputs = func_obj.input_params(); + + let mut params_json = json!({ }); + for input in inputs { + let mut iter = params_vec.iter(); + let _param = iter.find(|x| x.trim_start_matches('-') == input.name) + .ok_or(format!(r#"argument "{}" of type "{}" not found"#, input.name, input.kind))?; + + let value = iter.next() + .ok_or(format!(r#"argument "{}" of type "{}" has no value"#, input.name, input.kind))? + .to_string(); + + let value = match input.kind { + ParamType::Uint(_) | ParamType::Int(_) => { + json!(parse_integer_param(&value)?) + }, + ParamType::Array(ref x) => { + if let ParamType::Uint(_) = **x { + let mut result_vec: Vec = vec![]; + for i in value.split(|c| c == ',' || c == '[' || c == ']') { + if i != "" { + result_vec.push(parse_integer_param(i)?) + } + } + json!(result_vec) + } else { + json!(value) + } + }, + _ => { + json!(value) + } + }; + params_json[input.name.clone()] = value; + } + + serde_json::to_string(¶ms_json).map_err(|e| format!("{}", e)) +} + +pub fn parse_params(params_vec: Vec<&str>, abi: &str, method: &str) -> Result { + if params_vec.len() == 1 { + // if there is only 1 parameter it must be a json string with arguments + Ok(params_vec[0].to_owned()) + } else { + build_json_from_params(params_vec, abi, method) + } } \ No newline at end of file diff --git a/src/convert.rs b/src/convert.rs new file mode 100644 index 00000000..aeb41e93 --- /dev/null +++ b/src/convert.rs @@ -0,0 +1,22 @@ + +pub fn convert_token(amount: &str) -> Result { + let parts: Vec<&str> = amount.split(".").collect(); + if parts.len() >= 1 && parts.len() <= 2 { + let mut result = String::new(); + result += parts[0]; + if parts.len() == 2 { + let fraction = format!("{:0<9}", parts[1]); + if fraction.len() != 9 { + return Err("invalid fractional part".to_string()); + } + result += &fraction; + } else { + result += "000000000"; + } + u64::from_str_radix(&result, 10) + .map_err(|e| format!("failed to parse amount: {}", e))?; + + return Ok(result); + } + Err("Invalid amout value".to_string()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 51877357..94cf01e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ extern crate serde_derive; mod account; mod call; mod config; +mod convert; mod crypto; mod deploy; mod genaddr; @@ -28,8 +29,8 @@ mod helpers; mod voting; use account::get_account; -use call::{call_contract, call_contract_with_msg, generate_message}; -use clap::ArgMatches; +use call::{call_contract, call_contract_with_msg, generate_message, parse_params}; +use clap::{ArgMatches, SubCommand, Arg, AppSettings}; use config::{Config, set_config}; use crypto::{generate_mnemonic, extract_pubkey, generate_keypair}; use deploy::deploy_contract; @@ -73,6 +74,24 @@ fn main_internal() -> Result <(), String> { None => "none", }; + let callex_sub_command = SubCommand::with_name("callex") + .about("Sends external message to contract with encoded function call.") + .setting(AppSettings::AllowMissingPositional) + .setting(AppSettings::AllowLeadingHyphen) + .setting(AppSettings::TrailingVarArg) + .setting(AppSettings::DontCollapseArgsInUsage) + .arg(Arg::with_name("METHOD") + .help("Name of the calling method.")) + .arg(Arg::with_name("ADDRESS") + .help("Contract address.")) + .arg(Arg::with_name("ABI") + .help("Path to contract ABI file.")) + .arg(Arg::with_name("SIGN") + .help("Path to keypair file used to sign message.")) + .arg(Arg::with_name("PARAMS") + .help("Method arguments. Must be a list of --name value ... pairs or a json string with all arguments.") + .multiple(true)); + let matches = clap_app! (tonlabs_cli => (version: &*format!("0.1 ({})", build_info)) (author: "TONLabs") @@ -129,6 +148,7 @@ fn main_internal() -> Result <(), String> { (@arg WC: --wc +takes_value "Workchain id of the smart contract (default 0).") (@arg VERBOSE: -v --verbose "Prints additional information about command execution.") ) + (subcommand: callex_sub_command) (@subcommand call => (@setting AllowLeadingHyphen) (about: "Sends external message to contract with encoded function call.") @@ -223,6 +243,9 @@ fn main_internal() -> Result <(), String> { return convert_tokens(m); } } + if let Some(m) = matches.subcommand_matches("callex") { + return callex_command(m, conf); + } if let Some(m) = matches.subcommand_matches("call") { return call_command(m, conf, CallType::Call); } @@ -283,25 +306,9 @@ fn main_internal() -> Result <(), String> { fn convert_tokens(matches: &ArgMatches) -> Result<(), String> { let amount = matches.value_of("AMOUNT").unwrap(); - let parts: Vec<&str> = amount.split(".").collect(); - if parts.len() >= 1 && parts.len() <= 2 { - let mut result = String::new(); - result += parts[0]; - if parts.len() == 2 { - let fraction = format!("{:0<9}", parts[1]); - if fraction.len() != 9 { - return Err("invalid fractional part".to_string()); - } - result += &fraction; - } else { - result += "000000000"; - } - u64::from_str_radix(&result, 10) - .map_err(|e| format!("failed to parse amount: {}", e))?; - println!("{}", result); - return Ok(()); - } - return Err("Invalid amout value".to_string()); + let result = convert::convert_token(amount)?; + println!("{}", result); + Ok(()) } fn genphrase_command(_matches: &ArgMatches, _config: Config) -> Result<(), String> { @@ -398,6 +405,41 @@ fn call_command(matches: &ArgMatches, config: Config, call: CallType) -> Result< } } +fn callex_command(matches: &ArgMatches, config: Config) -> Result<(), String> { + let method = matches.value_of("METHOD"); + let address = Some( + matches.value_of("ADDRESS") + .map(|s| s.to_string()) + .or(config.addr.clone()) + .ok_or("ADDRESS is not defined. Supply it in config file or in command line.".to_string())? + ); + let abi = Some( + matches.value_of("ABI") + .map(|s| s.to_string()) + .or(config.abi_path.clone()) + .ok_or("ABI is not defined. Supply it in config file or in command line.".to_string())? + ); + let loaded_abi = std::fs::read_to_string(abi.as_ref().unwrap()) + .map_err(|e| format!("failed to read ABI file: {}", e.to_string()))?; + let params = Some(parse_params( + matches.values_of("PARAMS").unwrap().collect::>(), &loaded_abi, method.clone().unwrap() + )?); + let keys = matches.value_of("SIGN") + .map(|s| s.to_string()) + .or(config.keys_path.clone()); + + print_args!(matches, address, method, params, abi, keys); + call_contract( + config, + &address.unwrap(), + loaded_abi, + method.unwrap(), + ¶ms.unwrap(), + keys, + false, + ) +} + fn deploy_command(matches: &ArgMatches, config: Config) -> Result<(), String> { let tvc = matches.value_of("TVC"); let params = matches.value_of("PARAMS"); diff --git a/tests/cli.rs b/tests/cli.rs index 5951e8e3..8913ebea 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -187,5 +187,65 @@ fn test_deploy() -> Result<(), Box> { .stdout(predicate::str::contains(precalculated_addr)) .stdout(predicate::str::contains("Transaction succeeded.")); + Ok(()) +} + +#[test] +fn test_callex() -> Result<(), Box> { + let giver_abi_name = "tests/samples/giver.abi.json"; + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("callex") + .arg("sendGrams") + .arg("0:841288ed3b55d9cdafa806807f02a0ae0c169aa5edfe88a789a6482429756a94") + .arg(giver_abi_name) + .arg("--") + .arg("--dest") + .arg("0:1b91c010f35b1f5b42a05ad98eb2df80c302c37df69651e1f5ac9c69b7e90d4e") + .arg("--amount") + .arg("0.2T"); + cmd.assert() + .success() + .stdout(predicate::str::contains(r#""dest":"0:1b91c010f35b1f5b42a05ad98eb2df80c302c37df69651e1f5ac9c69b7e90d4e""#)) + .stdout(predicate::str::contains(r#""amount":"0200000000""#)) + .stdout(predicate::str::contains("Succeeded")); + + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("callex") + .arg("sendGrams") + .arg("0:841288ed3b55d9cdafa806807f02a0ae0c169aa5edfe88a789a6482429756a94") + .arg(giver_abi_name) + .arg("--") + .arg("--dest") + .arg("0:1b91c010f35b1f5b42a05ad98eb2df80c302c37df69651e1f5ac9c69b7e90d4e") + .arg("--amount") + .arg("1000000000"); + cmd.assert() + .success(); + cmd.assert() + .success() + .stdout(predicate::str::contains(r#""dest":"0:1b91c010f35b1f5b42a05ad98eb2df80c302c37df69651e1f5ac9c69b7e90d4e""#)) + .stdout(predicate::str::contains(r#""amount":"1000000000""#)) + .stdout(predicate::str::contains("Succeeded")); + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("callex") + .arg("sendGrams") + .arg("0:841288ed3b55d9cdafa806807f02a0ae0c169aa5edfe88a789a6482429756a94") + .arg(giver_abi_name) + .arg("--") + .arg("--dest") + .arg("0:1b91c010f35b1f5b42a05ad98eb2df80c302c37df69651e1f5ac9c69b7e90d4e") + .arg("--amount") + .arg("0x10000"); + cmd.assert() + .success(); + cmd.assert() + .success() + .stdout(predicate::str::contains(r#""dest":"0:1b91c010f35b1f5b42a05ad98eb2df80c302c37df69651e1f5ac9c69b7e90d4e""#)) + .stdout(predicate::str::contains(r#""amount":"0x10000""#)) + .stdout(predicate::str::contains("Succeeded")); + Ok(()) } \ No newline at end of file