Skip to content

Commit

Permalink
Add callex cmd with user-friendly params (#16)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
Keshoid authored May 19, 2020
1 parent 5670e9a commit 34eddea
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 22 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 }
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ If `--abi` or `--sign` option is omitted in parameters it must present in config

tonos-cli call [--abi <abi_file>] [--sign <keyfile>] <address> <method> <params>

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 <method> [<address>] [<abi>] [<keys>] 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 <abi_file>] <address> <method> <params>
Expand Down
65 changes: 65 additions & 0 deletions src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> {
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<String, String> {
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<String> = 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(&params_json).map_err(|e| format!("{}", e))
}

pub fn parse_params(params_vec: Vec<&str>, abi: &str, method: &str) -> Result<String, String> {
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)
}
}
22 changes: 22 additions & 0 deletions src/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

pub fn convert_token(amount: &str) -> Result<String, String> {
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())
}
84 changes: 63 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ extern crate serde_derive;
mod account;
mod call;
mod config;
mod convert;
mod crypto;
mod deploy;
mod genaddr;
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;
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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::<Vec<_>>(), &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(),
&params.unwrap(),
keys,
false,
)
}

fn deploy_command(matches: &ArgMatches, config: Config) -> Result<(), String> {
let tvc = matches.value_of("TVC");
let params = matches.value_of("PARAMS");
Expand Down
60 changes: 60 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,65 @@ fn test_deploy() -> Result<(), Box<dyn std::error::Error>> {
.stdout(predicate::str::contains(precalculated_addr))
.stdout(predicate::str::contains("Transaction succeeded."));

Ok(())
}

#[test]
fn test_callex() -> Result<(), Box<dyn std::error::Error>> {
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(())
}

0 comments on commit 34eddea

Please sign in to comment.