diff --git a/.gitignore b/.gitignore index 354a2550..5e6ca74f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ tests/PipechainTest1.chain *.txns *.log *.boc +!tests/samples/SafeMultisigWallet_msg.boc \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a0ea623f..b7b49844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## Version: 0.28.12 + +### New +- Added ability to specify link to the abi file of json data instead of path. + + ## Version: 0.28.3 ### Breaking changes: diff --git a/Cargo.lock b/Cargo.lock index 496dac6a..c9e55e02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,13 +375,13 @@ dependencies = [ [[package]] name = "console" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", "terminal_size", "winapi", ] @@ -1109,9 +1109,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.133" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "libsecp256k1" @@ -1386,9 +1386,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.41" +version = "0.10.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" dependencies = [ "bitflags", "cfg-if", @@ -1418,9 +1418,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.75" +version = "0.9.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" dependencies = [ "autocfg", "cc", @@ -1583,9 +1583,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -2066,9 +2066,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", @@ -2137,18 +2137,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -2202,9 +2202,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -2212,7 +2212,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -2540,7 +2539,7 @@ dependencies = [ [[package]] name = "tonos-cli" -version = "0.28.9" +version = "0.28.13" dependencies = [ "assert_cmd", "async-trait", @@ -2578,6 +2577,7 @@ dependencies = [ "ton_sdk", "ton_types", "ton_vm", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5d8483a7..9dac8426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ license = 'Apache-2.0' name = 'tonos-cli' readme = 'README.md' repository = 'https://github.com/tonlabs/tonos-cli' -version = '0.28.12' +version = '0.28.13' [dependencies] async-trait = '0.1.42' @@ -43,6 +43,7 @@ tokio-retry = '0.3' log = { features = [ 'std' ], version = '0.4.11' } serde = { features = [ 'derive' ], version = '1.0' } tokio = { default-features = false, features = [ 'full' ], version = '1' } +url = '2.3.1' ton_abi = { git = 'https://github.com/tonlabs/ton-labs-abi.git', tag = '2.3.7' } ton_block = { git = 'https://github.com/tonlabs/ton-labs-block.git', tag = '1.8.3' } ton_client = { git = 'https://github.com/tonlabs/TON-SDK.git', tag = '1.37.2' } diff --git a/README.md b/README.md index 24f9aba9..6f2661fb 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ subcommand. List of available options: ```bash ---abi Path to the contract ABI file. +--abi Path or link to the contract ABI file or pure json ABI data. --access_key Project secret or JWT in Evercloud (dashboard.evercloud.dev). --addr Contract address. --async_call Disables wait for transaction to appear in the network after call command. @@ -932,7 +932,7 @@ or `--wc ` ID of the workchain the wallet will be deployed to (`-1` for masterchain, `0` for basechain). By default, this value is set to 0. -`--abi ` - contract interface file. +`--abi ` - Path or link to the contract ABI file or pure json ABI data. Can be specified in the config file. `--alias ` - allows to save contract parameters (address, abi, keys) to use them easier with `callx` or `runx` commands. diff --git a/src/call.rs b/src/call.rs index bde42657..dcacb3ea 100644 --- a/src/call.rs +++ b/src/call.rs @@ -12,19 +12,10 @@ */ use crate::config::Config; use crate::convert; -use crate::helpers::{ - TonClient, now_ms, create_client_verbose, load_abi, - query_account_field, SDK_EXECUTION_ERROR_CODE, create_client -}; -use ton_abi::{Contract, ParamType}; - -use ton_client::abi::{ - encode_message, - decode_message, - ParamsOfDecodeMessage, - ParamsOfEncodeMessage, - Abi, -}; +use crate::helpers::{TonClient, now_ms, create_client_verbose, load_abi, query_account_field, SDK_EXECUTION_ERROR_CODE, create_client, load_ton_abi}; + +use ton_client::abi::{encode_message, decode_message, ParamsOfDecodeMessage, ParamsOfEncodeMessage, + Abi}; use ton_client::processing::{ ParamsOfSendMessage, ParamsOfWaitForTransaction, @@ -42,6 +33,7 @@ use ton_block::{Account, Serializable, Deserializable, Message}; use std::str::FromStr; use clap::ArgMatches; use serde_json::{Value}; +use ton_abi::ParamType; use ton_client::error::ClientError; use crate::debug::{execute_debug, DebugLogger}; use crate::message::{EncodedMessage, prepare_message_params, print_encoded_message, unpack_message}; @@ -76,8 +68,8 @@ fn parse_integer_param(value: &str) -> Result { } } -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))?; +async fn build_json_from_params(params_vec: Vec<&str>, abi_path: &str, method: &str) -> Result { + let abi_obj = load_ton_abi(abi_path).await?; let functions = abi_obj.functions(); let func_obj = functions.get(method).ok_or("failed to load function from abi")?; @@ -182,7 +174,7 @@ pub async fn send_message_and_wait( abi: Option, msg: String, config: &Config, -) -> Result { +) -> Result { if !config.is_json { println!("Processing... "); @@ -225,7 +217,7 @@ pub async fn process_message( ton: TonClient, msg: ParamsOfEncodeMessage, config: &Config, -) -> Result { +) -> Result { let callback = |event| { async move { if let ProcessingEvent::DidSend { shard_block_id: _, message_id, message: _ } = event { println!("MessageId: {}", message_id) @@ -259,13 +251,13 @@ pub async fn process_message( pub async fn call_contract_with_result( config: &Config, addr: &str, - abi: String, + abi_path: &str, method: &str, params: &str, keys: Option, is_fee: bool, matches: Option<&ArgMatches<'_>>, -) -> Result { +) -> Result { let ton = if config.debug_fail != "None".to_string() { let log_path = format!("call_{}_{}.log", addr, method); log::set_max_level(log::LevelFilter::Trace); @@ -276,21 +268,21 @@ pub async fn call_contract_with_result( } else { create_client_verbose(config)? }; - call_contract_with_client(ton, config, addr, abi, method, params, keys, is_fee, matches).await + call_contract_with_client(ton, config, addr, abi_path, method, params, keys, is_fee, matches).await } pub async fn call_contract_with_client( ton: TonClient, config: &Config, addr: &str, - abi_string: String, + abi_path: &str, method: &str, params: &str, keys: Option, is_fee: bool, matches: Option<&ArgMatches<'_>>, -) -> Result { - let abi = load_abi(&abi_string)?; +) -> Result { + let abi = load_abi(abi_path).await?; let msg_params = prepare_message_params( addr, @@ -350,8 +342,8 @@ pub async fn call_contract_with_client( && res.clone().err().unwrap().code == SDK_EXECUTION_ERROR_CODE { if config.is_json { let e = format!("{:#}", res.clone().err().unwrap()); - let err: serde_json::Value = serde_json::from_str(&e) - .unwrap_or(serde_json::Value::String(e)); + let err: Value = serde_json::from_str(&e) + .unwrap_or(Value::String(e)); let res = json!({"Error": err}); println!("{}", serde_json::to_string_pretty(&res) .unwrap_or("{{ \"JSON serialization error\" }}".to_string())); @@ -390,14 +382,14 @@ pub fn print_json_result(result: Value, config: &Config) -> Result<(), String> { pub async fn call_contract( config: &Config, addr: &str, - abi: String, + abi_path: &str, method: &str, params: &str, keys: Option, is_fee: bool, matches: Option<&ArgMatches<'_>>, ) -> Result<(), String> { - let result = call_contract_with_result(config, addr, abi, method, params, keys, is_fee, matches).await?; + let result = call_contract_with_result(config, addr, abi_path, method, params, keys, is_fee, matches).await?; if !config.is_json { println!("Succeeded."); } @@ -406,9 +398,9 @@ pub async fn call_contract( } -pub async fn call_contract_with_msg(config: &Config, str_msg: String, abi: String) -> Result<(), String> { +pub async fn call_contract_with_msg(config: &Config, str_msg: String, abi_path: &str) -> Result<(), String> { let ton = create_client_verbose(&config)?; - let abi = load_abi(&abi)?; + let abi = load_abi(abi_path).await?; let (msg, _) = unpack_message(&str_msg)?; if config.is_json { @@ -439,11 +431,11 @@ pub async fn call_contract_with_msg(config: &Config, str_msg: String, abi: Strin Ok(()) } -pub fn parse_params(params_vec: Vec<&str>, abi: &str, method: &str) -> Result { +pub async fn parse_params(params_vec: Vec<&str>, abi_path: &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) + build_json_from_params(params_vec, abi_path, method).await } } diff --git a/src/config.rs b/src/config.rs index 63439c27..bd62df2e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -372,10 +372,11 @@ impl FullConfig { pub fn clear_config( full_config: &mut FullConfig, - matches: &ArgMatches + matches: &ArgMatches, + is_json: bool, ) -> Result<(), String> { let mut config = &mut full_config.config; - let is_json = config.is_json; + let is_json = config.is_json || is_json; if matches.is_present("URL") { let url = default_url(); config.endpoints = FullConfig::default_map()[&url].clone(); @@ -464,7 +465,8 @@ pub fn clear_config( pub fn set_config( full_config: &mut FullConfig, - matches: &ArgMatches + matches: &ArgMatches, + is_json: bool, ) -> Result<(), String> { let mut config= &mut full_config.config; if let Some(s) = matches.value_of("URL") { @@ -569,13 +571,13 @@ pub fn set_config( } if let Some(s) = matches.value_of("ACCESS_KEY") { config.access_key = Some(s.to_string()); - if config.project_id.is_none() && !config.is_json { + if config.project_id.is_none() && !(config.is_json || is_json) { println!("Warning: You have access_key set without project_id. It has no sense in case of authentication."); } } full_config.to_file(&full_config.path)?; - if !full_config.config.is_json { + if !(full_config.config.is_json || is_json) { println!("Succeeded."); } Ok(()) diff --git a/src/debot/term_browser.rs b/src/debot/term_browser.rs index 1d573f54..cecc6253 100644 --- a/src/debot/term_browser.rs +++ b/src/debot/term_browser.rs @@ -114,7 +114,7 @@ impl TerminalBrowser { let info: DebotInfo = dengine.init().await?.into(); let abi_version = info.dabi_version.clone(); let abi_ref = info.dabi.as_ref(); - let abi = load_abi(abi_ref.ok_or("DeBot ABI is not defined".to_string())?)?; + let abi = load_abi(abi_ref.ok_or("DeBot ABI is not defined".to_string())?).await?; if !autorun { Self::print_info(&info); } diff --git a/src/debug.rs b/src/debug.rs index 92f50cd8..ff5140a3 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -136,7 +136,7 @@ pub fn create_debug_command<'a, 'b>() -> App<'a, 'b> { let abi_arg = Arg::with_name("ABI") .long("--abi") .takes_value(true) - .help("Path to the contract ABI file. Can be specified in the config file."); + .help("Path or link to the contract ABI file or pure json ABI data. Can be specified in the config file."); let decode_abi_arg = Arg::with_name("DECODE_ABI") .long("--decode_abi") @@ -201,6 +201,11 @@ pub fn create_debug_command<'a, 'b>() -> App<'a, 'b> { .long("--now") .help("Now timestamp (in milliseconds) for execution. If not set it is equal to the current timestamp."); + let ignore_hashes = Arg::with_name("IGNORE_HASHES") + .help("Ignore hashes mismatch. This flag must be set while replaying a contract after setcode on the Node SE.") + .long("--ignore_hashes") + .short("-i"); + let msg_cmd = SubCommand::with_name("message") .about("Play message locally with trace") .arg(output_arg.clone()) @@ -282,7 +287,8 @@ pub fn create_debug_command<'a, 'b>() -> App<'a, 'b> { .arg(decode_abi_arg.clone()) .arg(tx_id_arg.clone()) .arg(dump_config_arg.clone()) - .arg(dump_contract_arg.clone())) + .arg(dump_contract_arg.clone()) + .arg(ignore_hashes.clone())) .subcommand(SubCommand::with_name("account") .about("Loads list of the last transactions for the specified account. User should choose which one to debug.") .arg(empty_config_arg.clone()) @@ -294,7 +300,8 @@ pub fn create_debug_command<'a, 'b>() -> App<'a, 'b> { .arg(decode_abi_arg.clone()) .arg(address_arg.clone()) .arg(dump_config_arg.clone()) - .arg(dump_contract_arg.clone())) + .arg(dump_contract_arg.clone()) + .arg(ignore_hashes.clone())) .subcommand(SubCommand::with_name("replay") .about("Replay transaction on the saved account state.") .arg(output_arg.clone()) @@ -390,7 +397,7 @@ async fn debug_transaction_command(matches: &ArgMatches<'_>, config: &Config, is if !config.is_json { println!("Fetching config contract transactions..."); } - fetch(&config.url, CONFIG_ADDR, DEFAULT_CONFIG_PATH, is_empty_config, None, true).await?; + fetch(config, CONFIG_ADDR, DEFAULT_CONFIG_PATH, is_empty_config, None, true).await?; DEFAULT_CONFIG_PATH } }; @@ -402,7 +409,7 @@ async fn debug_transaction_command(matches: &ArgMatches<'_>, config: &Config, is if !config.is_json { println!("Fetching contract transactions..."); } - fetch(&config.url, &address, DEFAULT_CONTRACT_PATH, false, None, true).await?; + fetch(config, &address, DEFAULT_CONTRACT_PATH, false, None, true).await?; DEFAULT_CONTRACT_PATH } }; @@ -430,16 +437,19 @@ async fn debug_transaction_command(matches: &ArgMatches<'_>, config: &Config, is contract_path, config_path, &tx_id, - false, generate_callback(Some(matches), config), init_logger, dump_mask, - if is_empty_config { Some(&config) } else { None }, + config, + is_empty_config, + !matches.is_present("IGNORE_HASHES"), ).await?; decode_messages(tr.out_msgs, load_decode_abi(matches, config)).await?; if !config.is_json { println!("Log saved to {}.", trace_path); + } else { + println!("{{}}"); } Ok(()) } @@ -576,14 +586,13 @@ async fn debug_call_command(matches: &ArgMatches<'_>, full_config: &FullConfig, .ok_or("Method is not defined. Supply it in the config file or command line.")?); let is_boc = matches.is_present("BOC"); let is_tvc = matches.is_present("TVC"); - let loaded_abi = std::fs::read_to_string(opt_abi.as_ref().unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; + let loaded_abi = load_abi(opt_abi.as_ref().unwrap()).await?; let params = unpack_alternative_params( matches, - &loaded_abi, + opt_abi.as_ref().unwrap(), method.unwrap(), &full_config.config - )?; + ).await?; if !full_config.config.is_json { print_args!(input, method, params, sign, opt_abi, output); @@ -605,9 +614,6 @@ async fn debug_call_command(matches: &ArgMatches<'_>, full_config: &FullConfig, .map_err(|e| format!("Failed to construct account: {}", e))? }; - let abi = std::fs::read_to_string(&opt_abi.clone().unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; - let abi = load_abi(&abi)?; let params = serde_json::from_str(¶ms.unwrap()) .map_err(|e| format!("params are not in json format: {}", e))?; @@ -626,7 +632,7 @@ async fn debug_call_command(matches: &ArgMatches<'_>, full_config: &FullConfig, header: Some(header) }; let msg_params = ParamsOfEncodeMessage { - abi, + abi: loaded_abi, address: Some(format!("0:{}", "0".repeat(64))), // TODO: add option or get from input call_set: Some(call_set), signer: if keys.is_some() { @@ -809,14 +815,12 @@ async fn debug_deploy_command(matches: &ArgMatches<'_>, config: &Config) -> Resu let sign = matches.value_of("KEYS") .map(|s| s.to_string()) .or(config.keys_path.clone()); - let loaded_abi = std::fs::read_to_string(opt_abi.as_ref().unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; let params = unpack_alternative_params( matches, - &loaded_abi, + opt_abi.as_ref().unwrap(), "constructor", config - )?; + ).await?; let wc = Some(wc_from_matches_or_config(matches, config)?); if !config.is_json { print_args!(tvc, params, sign, opt_abi, output, debug_info); diff --git a/src/decode.rs b/src/decode.rs index 3e8d90b8..5f8e02e5 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -10,17 +10,15 @@ * See the License for the specific TON DEV software governing permissions and * limitations under the License. */ -use crate::{print_args}; +use crate::{load_abi, print_args}; use crate::config::Config; -use crate::helpers::{decode_msg_body, print_account, create_client_local, create_client_verbose, - query_account_field, abi_from_matches_or_config, load_ton_address}; +use crate::helpers::{decode_msg_body, print_account, create_client_local, create_client_verbose, query_account_field, abi_from_matches_or_config, load_ton_address, load_ton_abi}; use clap::{ArgMatches, SubCommand, Arg, App, AppSettings}; use ton_types::cells_serialization::serialize_tree_of_cells; use ton_types::{Cell, SliceData}; -use std::fs::File; use std::io::Cursor; use ton_block::{Account, Deserializable, Serializable, AccountStatus, StateInit}; -use ton_client::abi::{decode_account_data, ParamsOfDecodeAccountData, Abi}; +use ton_client::abi::{decode_account_data, ParamsOfDecodeAccountData}; use crate::decode::msg_printer::tree_of_cells_into_base64; use serde::Serialize; @@ -52,7 +50,7 @@ pub fn create_decode_command<'a, 'b>() -> App<'a, 'b> { .arg(Arg::with_name("ABI") .long("--abi") .takes_value(true) - .help("Path to the contract ABI file."))) + .help("Path or link to the contract ABI file or pure json ABI data. Can be specified in the config file."))) .subcommand(SubCommand::with_name("msg") .about("Decodes message file.") .arg(Arg::with_name("MSG") @@ -61,7 +59,7 @@ pub fn create_decode_command<'a, 'b>() -> App<'a, 'b> { .arg(Arg::with_name("ABI") .long("--abi") .takes_value(true) - .help("Path to the contract ABI file. Can be specified in the config file.")) + .help("Path or link to the contract ABI file or pure json ABI data. Can be specified in the config file.")) .arg(Arg::with_name("BASE64") .long("--base64") .help("Flag that changes behavior of the command to work with data in base64."))) @@ -86,7 +84,7 @@ pub fn create_decode_command<'a, 'b>() -> App<'a, 'b> { .arg(Arg::with_name("ABI") .long("--abi") .takes_value(true) - .help("Path to the contract ABI file."))) + .help("Path or link to the contract ABI file or pure json ABI data. Can be specified in the config file."))) .subcommand(SubCommand::with_name("boc") .about("Decodes data from the file with boc of the account and saves contract tvc file if needed.") .arg(Arg::with_name("BOCFILE") @@ -253,8 +251,7 @@ async fn decode_tvc_fields(m: &ArgMatches<'_>, config: &Config) -> Result<(), St if !config.is_json { print_args!(tvc, abi); } - let abi = std::fs::read_to_string(abi.unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; + let abi = load_abi(abi.as_ref().unwrap()).await?; let state = StateInit::construct_from_file(tvc.unwrap()) .map_err(|e| format!("failed to load StateInit from the tvc file: {}", e))?; let b64 = tree_of_cells_into_base64(state.data.as_ref())?; @@ -262,7 +259,7 @@ async fn decode_tvc_fields(m: &ArgMatches<'_>, config: &Config) -> Result<(), St let res = decode_account_data( ton, ParamsOfDecodeAccountData { - abi: Abi::Json(abi), + abi, data: b64, ..Default::default() } @@ -283,8 +280,7 @@ async fn decode_account_fields(m: &ArgMatches<'_>, config: &Config) -> Result<() if !config.is_json { print_args!(address, abi); } - let abi = std::fs::read_to_string(abi.unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; + let abi = load_abi(abi.as_ref().unwrap()).await?; let ton = create_client_verbose(&config)?; let address = load_ton_address(address.unwrap(), &config)?; @@ -293,7 +289,7 @@ async fn decode_account_fields(m: &ArgMatches<'_>, config: &Config) -> Result<() let res = decode_account_data( ton, ParamsOfDecodeAccountData { - abi: Abi::Json(abi), + abi, data, ..Default::default() } @@ -328,13 +324,10 @@ async fn decode_body(body_base64: &str, abi_path: &str, is_json: bool) -> Result let ton = create_client_local()?; - let abi = std::fs::read_to_string(abi_path) - .map_err(|e| format!("failed to read ABI file: {}", e))?; - let (mut res, is_external) = { - match decode_msg_body(ton.clone(), &abi, body_base64, false).await { + match decode_msg_body(ton.clone(), abi_path, body_base64, false).await { Ok(res) => (res, true), - Err(_) => (decode_msg_body(ton.clone(), &abi, body_base64, true).await?, false), + Err(_) => (decode_msg_body(ton.clone(), abi_path, body_base64, true).await?, false), } }; let mut signature = None; @@ -352,11 +345,7 @@ async fn decode_body(body_base64: &str, abi_path: &str, is_json: bool) -> Result } } } - let contr = File::open(abi_path).map(|file| { - ton_abi::Contract::load(file) - }) - .map_err(|e| format!("Failed to load abi: {}", e))? - .map_err(|e| format!("Failed to load abi: {}", e))?; + let contr = load_ton_abi(abi_path).await?; let (_, func_id, _) = ton_abi::Function::decode_header(contr.version(), orig_slice.clone(), contr.header(), !is_external) .map_err(|e| format!("Failed to decode header: {}", e))?; @@ -387,16 +376,10 @@ async fn decode_body(body_base64: &str, abi_path: &str, is_json: bool) -> Result Ok(()) } - - -async fn decode_message(msg_boc: Vec, abi: Option) -> Result { - let abi = abi.map(std::fs::read_to_string) - .transpose() - .map_err(|e| format!("failed to read ABI file: {}", e))?; - +async fn decode_message(msg_boc: Vec, abi_path: Option) -> Result { let tvm_msg = ton_sdk::Contract::deserialize_message(&msg_boc[..]) .map_err(|e| format!("failed to deserialize message boc: {}", e))?; - let result = msg_printer::serialize_msg(&tvm_msg, abi).await?; + let result = msg_printer::serialize_msg(&tvm_msg, abi_path).await?; Ok(serde_json::to_string_pretty(&result) .map_err(|e| format!("Failed to serialize the result: {}", e))?) } @@ -566,7 +549,7 @@ pub mod msg_printer { } } - pub async fn serialize_body(body_vec: Vec, abi: &str, ton: TonClient) -> Result { + pub async fn serialize_body(body_vec: Vec, abi_path: &str, ton: TonClient) -> Result { let mut empty_boc = vec![]; serialize_tree_of_cells(&Cell::default(), &mut empty_boc) .map_err(|e| format!("failed to serialize tree of cells: {}", e))?; @@ -575,16 +558,16 @@ pub mod msg_printer { } let body_base64 = base64::encode(&body_vec); let mut res = { - match decode_msg_body(ton.clone(), abi, &body_base64, false).await { + match decode_msg_body(ton.clone(), abi_path, &body_base64, false).await { Ok(res) => res, - Err(_) => decode_msg_body(ton.clone(), abi, &body_base64, true).await?, + Err(_) => decode_msg_body(ton.clone(), abi_path, &body_base64, true).await?, } }; let output = res.value.take().ok_or("failed to obtain the result")?; Ok(json!({res.name : output})) } - pub async fn serialize_msg(msg: &Message, abi: Option) -> Result { + pub async fn serialize_msg(msg: &Message, abi_path: Option) -> Result { let mut res = json!({ }); let ton = create_client_local()?; res["Type"] = serialize_msg_type(msg.header()); @@ -595,12 +578,12 @@ pub mod msg_printer { res["Body"] = json!(&tree_of_cells_into_base64( msg.body().map(|slice| slice.into_cell()).as_ref() )?); - if abi.is_some() && msg.body().is_some() { - let abi = abi.unwrap(); + if abi_path.is_some() && msg.body().is_some() { + let abi_path = abi_path.unwrap(); let mut body_vec = Vec::new(); serialize_tree_of_cells(&msg.body().unwrap().into_cell(), &mut body_vec) .map_err(|e| format!("failed to serialize body: {}", e))?; - res["BodyCall"] = match serialize_body(body_vec, &abi, ton).await { + res["BodyCall"] = match serialize_body(body_vec, &abi_path, ton).await { Ok(res) => res, Err(_) => { json!("Undefined") diff --git a/src/deploy.rs b/src/deploy.rs index ff3f99da..d3dc850e 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -54,9 +54,7 @@ pub async fn deploy_contract( } if config.async_call { - let abi = std::fs::read_to_string(abi) - .map_err(|e| format!("failed to read ABI file: {}", e))?; - let abi = load_abi(&abi)?; + let abi = load_abi(&abi).await?; send_message_and_wait(ton, Some(abi), enc_msg.message, @@ -118,9 +116,7 @@ pub async fn prepare_deploy_message( keys_file: Option, wc: i32 ) -> Result<(ParamsOfEncodeMessage, String), String> { - let abi = std::fs::read_to_string(abi) - .map_err(|e| format!("failed to read ABI file: {}", e))?; - let abi = load_abi(&abi)?; + let abi = load_abi(abi).await?; let keys = keys_file.map(|k| load_keypair(&k)).transpose()?; diff --git a/src/depool.rs b/src/depool.rs index 87627dea..cb8c5b58 100644 --- a/src/depool.rs +++ b/src/depool.rs @@ -403,7 +403,7 @@ async fn print_event(ton: TonClient, event: &serde_json::Value) -> Result<(), St let result = ton_client::abi::decode_message_body( ton.clone(), ParamsOfDecodeMessageBody { - abi: load_abi(DEPOOL_ABI).map_err(|e| format!("failed to load depool abi: {}", e))?, + abi: load_abi(DEPOOL_ABI).await.map_err(|e| format!("failed to load depool abi: {}", e))?, body: body.to_owned(), is_internal: false, ..Default::default() @@ -693,7 +693,7 @@ async fn encode_body(func: &str, params: serde_json::Value) -> Result Result<(), String> { let ton = create_client_verbose(&config)?; - let abi = load_abi(MSIG_ABI)?; + let abi = load_abi(MSIG_ABI).await?; let start = now()?; let params = json!({ diff --git a/src/genaddr.rs b/src/genaddr.rs index 794ec213..77c68dbc 100644 --- a/src/genaddr.rs +++ b/src/genaddr.rs @@ -11,7 +11,7 @@ * limitations under the License. */ use crate::config::Config; -use crate::helpers::{create_client_local, read_keys, load_abi, calc_acc_address}; +use crate::helpers::{create_client_local, read_keys, load_abi, calc_acc_address, load_abi_str}; use ed25519_dalek::PublicKey; use std::fs::OpenOptions; @@ -21,7 +21,7 @@ use ton_client::utils::{convert_address, ParamsOfConvertAddress, AddressStringFo pub async fn generate_address( config: &Config, tvc: &str, - abi: &str, + abi_path: &str, wc_str: Option<&str>, keys_file: Option<&str>, new_keys: bool, @@ -31,10 +31,7 @@ pub async fn generate_address( let contract = std::fs::read(tvc) .map_err(|e| format!("failed to read smart contract file: {}", e))?; - let abi_str = std::fs::read_to_string(abi) - .map_err(|e| format!("failed to read ABI file: {}", e))?; - - let abi = load_abi(&abi_str)?; + let abi = load_abi(abi_path).await?; let phrase = if new_keys { gen_seed_phrase()? @@ -82,7 +79,7 @@ pub async fn generate_address( vec![0; 32] } }; - + let abi_str = load_abi_str(abi_path).await?; update_contract_state(tvc, &key_bytes, initial_data, &abi_str)?; if !config.is_json { println!("TVC file updated"); diff --git a/src/helpers.rs b/src/helpers.rs index 01ab577e..26752f32 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -28,6 +28,8 @@ use ton_block::{Account, MsgAddressInt, Deserializable, CurrencyCollection, Stat use std::str::FromStr; use clap::ArgMatches; use serde_json::Value; +use ton_client::abi::Abi::Contract; +use url::Url; use crate::call::parse_params; use crate::FullConfig; @@ -118,8 +120,8 @@ pub fn create_client_local() -> Result { Ok(Arc::new(cli)) } -pub fn create_client(config: &Config) -> Result { - let modified_endpoints = if config.project_id.is_some() { +pub fn get_server_endpoints(config: &Config) -> Vec { + if config.project_id.is_some() { let mut cur_endpoints = match config.endpoints.len() { 0 => vec![config.url.clone()], _ => config.endpoints.clone(), @@ -134,7 +136,11 @@ pub fn create_client(config: &Config) -> Result { config.endpoints.clone().iter_mut().map(|end| { end.trim_end_matches('/').to_owned() }).collect::>() - }; + } +} + +pub fn create_client(config: &Config) -> Result { + let modified_endpoints = get_server_endpoints(config); if !config.is_json { println!("Connecting to:\n\tUrl: {}", config.url); println!("\tEndpoints: {:?}\n", modified_endpoints); @@ -268,11 +274,11 @@ pub async fn query_account_field(ton: TonClient, address: &str, field: &str) -> pub async fn decode_msg_body( ton: TonClient, - abi_str: &str, + abi_path: &str, body: &str, is_internal: bool, ) -> Result { - let abi = load_abi(abi_str)?; + let abi = load_abi(abi_path).await?; ton_client::abi::decode_message_body( ton, ParamsOfDecodeMessageBody { @@ -286,13 +292,44 @@ pub async fn decode_msg_body( .map_err(|e| format!("failed to decode body: {}", e)) } -pub fn load_abi(abi: &str) -> Result { - Ok(Abi::Contract( - serde_json::from_str::(abi) +pub async fn load_abi_str(abi_path: &str) -> Result { + let abi_from_json = serde_json::from_str::(abi_path); + if abi_from_json.is_ok() { + return Ok(abi_path.to_string()); + } + if Url::parse(abi_path).is_ok() { + let abi_bytes = load_file_with_url(abi_path).await?; + return Ok(String::from_utf8(abi_bytes) + .map_err(|e| format!("Downloaded string contains not valid UTF8 characters: {}", e))?); + } + Ok(std::fs::read_to_string(&abi_path) + .map_err(|e| format!("failed to read ABI file: {}", e))?) +} + +pub async fn load_abi(abi_path: &str) -> Result { + let abi_str = load_abi_str(abi_path).await?; + Ok(Contract(serde_json::from_str::(&abi_str) .map_err(|e| format!("ABI is not a valid json: {}", e))?, )) } +pub async fn load_ton_abi(abi_path: &str) -> Result { + let abi_str = load_abi_str(abi_path).await?; + Ok(ton_abi::Contract::load(abi_str.as_bytes()) + .map_err(|e| format!("Failed to load ABI: {}", e))?) +} + +pub async fn load_file_with_url(url: &str) -> Result, String> { + let response = reqwest::get(url) + .await + .map_err(|e| format!("Failed to download the file data: {}", e))?; + let responce_bytes = response.bytes() + .await + .map_err(|e| format!("Failed to decode network response: {}", e))?; + Ok(responce_bytes.to_vec()) +} + + pub async fn calc_acc_address( tvc: &[u8], wc: i32, @@ -350,7 +387,7 @@ pub fn events_filter(addr: &str, since: u32) -> serde_json::Value { }) } -pub async fn print_message(ton: TonClient, message: &serde_json::Value, abi: &str, is_internal: bool) -> Result<(String, String), String> { +pub async fn print_message(ton: TonClient, message: &Value, abi: &str, is_internal: bool) -> Result<(String, String), String> { println!("Id: {}", message["id"].as_str().unwrap_or("Undefined")); let value = message["value"].as_str().unwrap_or("0x0"); let value = u64::from_str_radix(value.trim_start_matches("0x"), 16) @@ -368,7 +405,7 @@ pub async fn print_message(ton: TonClient, message: &serde_json::Value, abi: &st let result = ton_client::abi::decode_message_body( ton.clone(), ParamsOfDecodeMessageBody { - abi: load_abi(abi)?, + abi: load_abi(abi).await?, body: body.to_owned(), is_internal, ..Default::default() @@ -611,10 +648,10 @@ pub fn load_params(params: &str) -> Result { }) } -pub fn unpack_alternative_params(matches: &ArgMatches<'_>, abi: &str, method: &str, config: &Config) -> Result, String> { +pub async fn unpack_alternative_params(matches: &ArgMatches<'_>, abi_path: &str, method: &str, config: &Config) -> Result, String> { if matches.is_present("PARAMS") { let params = matches.values_of("PARAMS").unwrap().collect::>(); - Ok(Some(parse_params(params, abi, method)?)) + Ok(Some(parse_params(params, abi_path, method).await?)) } else { Ok(config.parameters.clone().or(Some("{}".to_string()))) } diff --git a/src/main.rs b/src/main.rs index 7cce8da2..903cf264 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,7 +99,7 @@ async fn main_internal() -> Result <(), String> { let abi_arg = Arg::with_name("ABI") .long("--abi") .takes_value(true) - .help("Path to the contract ABI file. Can be specified in the config file."); + .help("Path or link to the contract ABI file or pure json ABI data. Can be specified in the config file."); let keys_arg = Arg::with_name("KEYS") .long("--keys") @@ -405,7 +405,7 @@ async fn main_internal() -> Result <(), String> { .help("Url to connect.")) .arg(Arg::with_name("ABI") .long("--abi") - .help("Path to the contract ABI file.")) + .help("Path or link to the contract ABI file or pure json ABI data.")) .arg(Arg::with_name("KEYS") .long("--keys") .help("Path to the file with keypair.")) @@ -486,7 +486,7 @@ async fn main_internal() -> Result <(), String> { .arg(Arg::with_name("ABI") .long("--abi") .takes_value(true) - .help("Path to the contract abi file.")) + .help("Path or link to the contract ABI file or pure json ABI data.")) .arg(Arg::with_name("KEYS") .long("--keys") .takes_value(true) @@ -536,7 +536,7 @@ async fn main_internal() -> Result <(), String> { .arg(Arg::with_name("ABI") .long("--abi") .takes_value(true) - .help("Path to the contract ABI file.")) + .help("Path or link to the contract ABI file or pure json ABI data.")) .arg(Arg::with_name("KEYS") .long("--keys") .takes_value(true) @@ -862,7 +862,11 @@ async fn main_internal() -> Result <(), String> { .arg(Arg::with_name("CURRENT_CONFIG") .help("Replay transaction with current network config.") .long("--current_config") - .short("-e")); + .short("-e")) + .arg(Arg::with_name("IGNORE_HASHES") + .help("Ignore hashes mismatch. This flag must be set while replaying a contract after setcode on the Node SE.") + .long("--ignore_hashes") + .short("-i")); let matches = App::new("tonos_cli") .version(&*format!("{}\nCOMMIT_ID: {}\nBUILD_DATE: {}\nCOMMIT_DATE: {}\nGIT_BRANCH: {}", @@ -961,6 +965,11 @@ async fn command_parser(matches: &ArgMatches<'_>, is_json: bool) -> Result <(), .unwrap_or(default_config_name()); let mut full_config = FullConfig::from_file(&config_file); + + if let Some(m) = matches.subcommand_matches("config") { + return config_command(m, full_config, is_json); + } + full_config.config.is_json = is_json || full_config.config.is_json; let config = &mut full_config.config; @@ -1004,9 +1013,6 @@ async fn command_parser(matches: &ArgMatches<'_>, is_json: bool) -> Result <(), if let Some(m) = matches.subcommand_matches("deploy_message") { return deploy_command(m, &mut full_config, DeployType::MsgOnly).await; } - if let Some(m) = matches.subcommand_matches("config") { - return config_command(m, full_config); - } if let Some(m) = matches.subcommand_matches("genaddr") { return genaddr_command(m, config).await; } @@ -1144,10 +1150,7 @@ async fn send_command(matches: &ArgMatches<'_>, config: &Config) -> Result<(), S print_args!(message, abi); } - let abi = std::fs::read_to_string(abi.unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; - - call_contract_with_msg(config, message.unwrap().to_owned(), abi).await + call_contract_with_msg(config, message.unwrap().to_owned(), &abi.unwrap()).await } async fn body_command(matches: &ArgMatches<'_>, config: &Config) -> Result<(), String> { @@ -1163,15 +1166,11 @@ async fn body_command(matches: &ArgMatches<'_>, config: &Config) -> Result<(), S let params = serde_json::from_str(¶ms.unwrap()) .map_err(|e| format!("arguments are not in json format: {}", e))?; - let abi = std::fs::read_to_string(abi.unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; - - let client = create_client_local()?; let body = ton_client::abi::encode_message_body( client.clone(), ParamsOfEncodeMessageBody { - abi: load_abi(&abi)?, + abi: load_abi(abi.as_ref().unwrap()).await?, call_set: CallSet::some_with_function_and_input(method.unwrap(), params) .ok_or("failed to create CallSet with specified parameters.")?, is_internal: true, @@ -1211,8 +1210,6 @@ async fn call_command(matches: &ArgMatches<'_>, config: &Config, call: CallType) if !config.is_json { print_args!(address, method, params, abi, keys, lifetime, output); } - let abi = std::fs::read_to_string(abi.unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; let address = load_ton_address(address.unwrap(), &config)?; match call { @@ -1221,7 +1218,7 @@ async fn call_command(matches: &ArgMatches<'_>, config: &Config, call: CallType) call_contract( config, address.as_str(), - abi, + &abi.unwrap(), method.unwrap(), ¶ms.unwrap(), keys, @@ -1240,7 +1237,7 @@ async fn call_command(matches: &ArgMatches<'_>, config: &Config, call: CallType) generate_message( config, address.as_str(), - abi, + &abi.unwrap(), method.unwrap(), ¶ms.unwrap(), keys, @@ -1257,15 +1254,12 @@ async fn callx_command(matches: &ArgMatches<'_>, full_config: &FullConfig) -> Re let method = Some(matches.value_of("METHOD").or(config.method.as_deref()) .ok_or("Method is not defined. Supply it in the config file or command line.")?); let (address, abi, keys) = contract_data_from_matches_or_config_alias(matches, full_config)?; - let loaded_abi = std::fs::read_to_string(abi.as_ref().unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; - let params = unpack_alternative_params( matches, - &loaded_abi, + abi.as_ref().unwrap(), method.unwrap(), config - )?; + ).await?; let params = Some(load_params(params.unwrap().as_ref())?); if !config.is_json { @@ -1277,7 +1271,7 @@ async fn callx_command(matches: &ArgMatches<'_>, full_config: &FullConfig) -> Re call_contract( config, address.as_str(), - loaded_abi, + &abi.unwrap(), &method.unwrap(), ¶ms.unwrap(), keys, @@ -1342,14 +1336,12 @@ async fn deployx_command(matches: &ArgMatches<'_>, full_config: &mut FullConfig) let tvc = matches.value_of("TVC"); let wc = wc_from_matches_or_config(matches, config)?; let abi = Some(abi_from_matches_or_config(matches, &config)?); - let loaded_abi = std::fs::read_to_string(abi.as_ref().unwrap()) - .map_err(|e| format!("failed to read ABI file: {}", e))?; let params = unpack_alternative_params( matches, - &loaded_abi, + abi.as_ref().unwrap(), "constructor", config - )?; + ).await?; let keys = matches.value_of("KEYS") .map(|s| s.to_string()) .or(config.keys_path.clone()); @@ -1362,14 +1354,14 @@ async fn deployx_command(matches: &ArgMatches<'_>, full_config: &mut FullConfig) deploy_contract(full_config, tvc.unwrap(), &abi.unwrap(), ¶ms.unwrap(), keys, wc, false, alias).await } -fn config_command(matches: &ArgMatches, mut full_config: FullConfig) -> Result<(), String> { +fn config_command(matches: &ArgMatches, mut full_config: FullConfig, is_json: bool) -> Result<(), String> { let mut result = Ok(()); if matches.is_present("GLOBAL") { full_config = FullConfig::from_file(&global_config_path()); } if !matches.is_present("LIST") { if let Some(clear_matches) = matches.subcommand_matches("clear") { - result = clear_config(&mut full_config, clear_matches); + result = clear_config(&mut full_config, clear_matches, is_json); } else if let Some(endpoint_matches) = matches.subcommand_matches("endpoint") { if let Some(endpoint_matches) = endpoint_matches.subcommand_matches("add") { let url = endpoint_matches.value_of("URL").unwrap(); @@ -1404,7 +1396,7 @@ fn config_command(matches: &ArgMatches, mut full_config: FullConfig) -> Result<( return Err("At least one option must be specified".to_string()); } - result = set_config(&mut full_config, matches); + result = set_config(&mut full_config, matches, is_json); } } println!( diff --git a/src/message.rs b/src/message.rs index eb9dfb6b..d74f180d 100644 --- a/src/message.rs +++ b/src/message.rs @@ -153,7 +153,7 @@ pub fn unpack_message(str_msg: &str) -> Result<(EncodedMessage, String), String> pub async fn generate_message( config: &Config, addr: &str, - abi: String, + abi: &str, method: &str, params: &str, keys: Option, @@ -166,7 +166,7 @@ pub async fn generate_message( let ton_addr = load_ton_address(addr, &config) .map_err(|e| format!("failed to parse address: {}", e.to_string()))?; - let abi = load_abi(&abi)?; + let abi = load_abi(abi).await?; let expire_at = lifetime + now()?; let header = FunctionHeader { diff --git a/src/multisig.rs b/src/multisig.rs index ff637134..68667423 100644 --- a/src/multisig.rs +++ b/src/multisig.rs @@ -15,7 +15,7 @@ use crate::call; use crate::config::Config; use crate::convert; use crate::deploy::prepare_deploy_message_params; -use crate::helpers::{create_client_local, load_abi, load_ton_address, create_client_verbose}; +use crate::helpers::{create_client_local, load_abi, load_ton_address, create_client_verbose, load_file_with_url}; use clap::{App, ArgMatches, SubCommand, Arg, AppSettings}; use ton_client::abi::{encode_message_body, ParamsOfEncodeMessageBody, CallSet}; use crate::crypto::load_keypair; @@ -263,7 +263,7 @@ async fn multisig_send_command(matches: &ArgMatches<'_>, config: &Config) -> Res pub async fn encode_transfer_body(text: &str) -> Result { let text = hex::encode(text.as_bytes()); let client = create_client_local()?; - let abi = load_abi(TRANSFER_WITH_COMMENT)?; + let abi = load_abi(TRANSFER_WITH_COMMENT).await?; encode_message_body( client.clone(), ParamsOfEncodeMessageBody { @@ -316,7 +316,7 @@ pub async fn send_with_body( call::call_contract( config, addr, - MSIG_ABI.to_string(), + MSIG_ABI, "submitTransaction", ¶ms, Some(keys.to_owned()), @@ -339,10 +339,8 @@ async fn multisig_deploy_command(matches: &ArgMatches<'_>, config: &Config) -> R SAFEMULTISIG_LINK }; - let response = reqwest::get(target).await.map_err(|e| format!("Failed to download the contract TVC: {}", e))?; - - let tvc_bytes = response.bytes().await.map_err(|e| format!("Failed to decode network response: {}", e))?; - let abi = load_abi(MSIG_ABI)?; + let tvc_bytes = load_file_with_url(target).await?; + let abi = load_abi(MSIG_ABI).await?; let keys = load_keypair(&keys)?; @@ -366,7 +364,7 @@ async fn multisig_deploy_command(matches: &ArgMatches<'_>, config: &Config) -> R matches.value_of("CONFIRMS").unwrap_or("1") ); - let (msg, address) = prepare_deploy_message_params(&tvc_bytes.to_vec(), abi, ¶m_str, Some(keys), config.wc).await?; + let (msg, address) = prepare_deploy_message_params(&tvc_bytes, abi, ¶m_str, Some(keys), config.wc).await?; if !config.is_json { println!("Wallet address: {}", address); @@ -380,7 +378,7 @@ async fn multisig_deploy_command(matches: &ArgMatches<'_>, config: &Config) -> R ton.clone(), config, LOCAL_GIVER_ADDR, - LOCAL_GIVER_TRANSFER.to_string(), + LOCAL_GIVER_TRANSFER, "sendGrams", ¶ms, None, diff --git a/src/replay.rs b/src/replay.rs index ca73d27d..a22b6f32 100644 --- a/src/replay.rs +++ b/src/replay.rs @@ -32,7 +32,7 @@ use ton_types::{UInt256, serialize_tree_of_cells}; use ton_vm::executor::{Engine, EngineTraceInfo}; use crate::config::Config; -use crate::helpers::{create_client, query_account_field}; +use crate::helpers::{create_client, get_server_endpoints, query_account_field}; pub static CONFIG_ADDR: &str = "-1:5555555555555555555555555555555555555555555555555555555555555555"; @@ -55,16 +55,19 @@ fn construct_blockchain_config_err(config_account: &Account) -> Result, rewrite_file: bool) -> Result<(), String> { +pub async fn fetch(config: &Config, account_address: &str, filename: &str, fast_stop: bool, lt_bound: Option, rewrite_file: bool) -> Result<(), String> { if !rewrite_file && std::path::Path::new(filename).exists() { - println!("File exists"); + if !config.is_json { + println!("File exists"); + } return Ok(()) } let context = Arc::new( ClientContext::new(ClientConfig { network: NetworkConfig { - server_address: Some(String::from(server_address)), + endpoints: Some(get_server_endpoints(config)), + access_key: config.access_key.clone(), ..Default::default() }, ..Default::default() @@ -141,7 +144,9 @@ pub async fn fetch(server_address: &str, account_address: &str, filename: &str, } if !zerostate_found { - println!("account {}: zerostate not found, writing out default initial state", account_address); + if !config.is_json { + println!("account {}: zerostate not found, writing out default initial state", account_address); + } let data = format!("{{\"id\":\"{}\",\"boc\":\"{}\"}}\n", account_address, base64::encode(&Account::default().write_to_bytes() .map_err(|e| format!("failed to serialize account: {}", e))?)); @@ -286,59 +291,38 @@ fn choose<'a>(st1: &'a mut State, st2: &'a mut State) -> &'a mut State { } } -struct TrivialLogger; -static LOGGER: TrivialLogger = TrivialLogger; - -impl log::Log for TrivialLogger { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - fn log(&self, record: &log::Record) { - println!("{}", record.args()); - } - fn flush(&self) {} -} - pub async fn replay( input_filename: &str, config_filename: &str, txnid: &str, - trace_execution: bool, trace_callback: Option>, init_trace_last_logger: impl Fn() -> Result<(), String>, dump_mask: u8, - cli_config: Option<&Config>, + cli_config: &Config, + load_current_net_config: bool, + exit_on_wrong_contract_hash: bool, ) -> Result { let mut account_state = State::new(input_filename)?; let account_address = account_state.account_addr.clone(); - - let (mut config, mut config_state) = match cli_config { - Some(cli_config) => { - let ton_client = create_client(cli_config)?; - let config = query_account_field( - ton_client.clone(), - CONFIG_ADDR, - "boc", - ).await?; - let config = Account::construct_from_base64(&config).map_err(|_| "".to_string())?; - let mut state = State::default()?; - state.set_account(config.clone()); - (construct_blockchain_config(&config)?, state) - }, - None => { - let config_state = State::new(config_filename)?; - assert_eq!(config_state.account_addr, CONFIG_ADDR); - (BlockchainConfig::default(), config_state) - }, + let (mut config, mut config_state) = if load_current_net_config { + let ton_client = create_client(cli_config)?; + let config = query_account_field( + ton_client.clone(), + CONFIG_ADDR, + "boc", + ).await?; + let config = Account::construct_from_base64(&config).map_err(|_| "".to_string())?; + let mut state = State::default()?; + state.set_account(config.clone()); + (construct_blockchain_config(&config)?, state) + } else { + let config_state = State::new(config_filename)?; + assert_eq!(config_state.account_addr, CONFIG_ADDR); + (BlockchainConfig::default(), config_state) }; let mut cur_block_lt = 0u64; - if trace_execution { - log::set_max_level(log::LevelFilter::Trace); - log::set_logger(&LOGGER).map_err(|e| format!("Failed to set logger: {}", e))?; - } - loop { if account_state.tr.is_none() { account_state.next_transaction(); @@ -354,7 +338,7 @@ pub async fn replay( let state = choose(&mut account_state, &mut config_state); let tr = state.tr.as_ref().ok_or("failed to obtain state transaction")?; - if cli_config.is_none() { + if !load_current_net_config { if cur_block_lt == 0 || cur_block_lt != tr.block_lt { assert!(tr.block_lt > cur_block_lt); cur_block_lt = tr.block_lt; @@ -369,23 +353,31 @@ pub async fn replay( let account_old_hash_remote = tr.tr.read_state_update() .map_err(|e| format!("failed to read state update: {}", e))?.old_hash; if account_old_hash_local != account_old_hash_remote { - println!("FAILURE\nOld hashes mismatch:\nremote {}\nlocal {}", - account_old_hash_remote.to_hex_string(), - account_old_hash_local.to_hex_string()); - exit(1); + if !cli_config.is_json { + println!("FAILURE\nOld hashes mismatch:\nremote {}\nlocal {}", + account_old_hash_remote.to_hex_string(), + account_old_hash_local.to_hex_string()); + } + if exit_on_wrong_contract_hash { + exit(1); + } } if tr.id == txnid { if dump_mask & DUMP_ACCOUNT != 0 { let path = format!("{}-{}.boc", account_address.split(':').last().unwrap_or(""), txnid); account_root.write_to_file(&path); - println!("Contract account was dumped to {}", path); + if !cli_config.is_json { + println!("Contract account was dumped to {}", path); + } } if dump_mask & DUMP_CONFIG != 0 { let path = format!("config-{}.boc", txnid); let account = config_account.serialize() .map_err(|e| format!("Failed to serialize config account: {}", e))?; account.write_to_file(&path); - println!("Config account was dumped to {}", path); + if !cli_config.is_json { + println!("Config account was dumped to {}", path); + } } if dump_mask & DUMP_EXECUTOR_CONFIG != 0 { // config.boc suitable for creating ton-labs-executor tests @@ -400,7 +392,9 @@ pub async fn replay( let path = format!("config-{}-test.boc", txnid); cfg.into_cell().map_err(|e| format!("Failed to finalize builder: {}", e))? .write_to_file(&path); - println!("Config for executor was dumped to {}", path); + if !cli_config.is_json { + println!("Config for executor was dumped to {}", path); + } } if trace_callback.is_some() { init_trace_last_logger()?; @@ -411,7 +405,6 @@ pub async fn replay( block_unixtime: tr.tr.now(), block_lt: tr.tr.logical_time(), last_tr_lt: Arc::new(AtomicU64::new(tr.tr.logical_time())), - debug: trace_execution, trace_callback, ..ExecuteParams::default() }; @@ -443,7 +436,6 @@ pub async fn replay( block_unixtime: tr.tr.now(), block_lt: tr.tr.logical_time(), last_tr_lt: Arc::new(AtomicU64::new(tr.tr.logical_time())), - debug: trace_execution, ..ExecuteParams::default() }; let tr_local = executor.execute_with_libs_and_params( @@ -460,19 +452,27 @@ pub async fn replay( .map_err(|e| format!("failed to read state update: {}", e))? .new_hash; if account_new_hash_local != account_new_hash_remote { - println!("FAILURE\nNew hashes mismatch:\nremote {}\nlocal {}", - account_new_hash_remote.to_hex_string(), - account_new_hash_local.to_hex_string()); - let local_desc = tr_local.read_description() - .map_err(|e| format!("failed to read description: {}", e))?; - let remote_desc = tr.tr.read_description() - .map_err(|e| format!("failed to read description: {}", e))?; - assert_eq!(remote_desc, local_desc); - exit(2); + if !cli_config.is_json { + println!("FAILURE\nNew hashes mismatch:\nremote {}\nlocal {}\nTR id: {}", + account_new_hash_remote.to_hex_string(), + account_new_hash_local.to_hex_string(), tr.id); + } + if exit_on_wrong_contract_hash { + let local_desc = tr_local.read_description() + .map_err(|e| format!("failed to read description: {}", e))?; + let remote_desc = tr.tr.read_description() + .map_err(|e| format!("failed to read description: {}", e))?; + assert_eq!(remote_desc, local_desc); + exit(2); + } } if tr.id == txnid { - println!("DONE"); + if !cli_config.is_json { + println!("DONE"); + } else { + println!("{{}}"); + } return Ok(tr_local); } state.tr = None; @@ -480,11 +480,12 @@ pub async fn replay( Err("Specified transaction was not found.".to_string()) } -pub async fn fetch_block(server_address: &str, block_id: &str, filename: &str) -> Result<(), failure::Error> { +pub async fn fetch_block(config: &Config, block_id: &str, filename: &str) -> Result<(), failure::Error> { let context = Arc::new( ClientContext::new(ClientConfig { network: NetworkConfig { - server_address: Some(String::from(server_address)), + endpoints: Some(get_server_endpoints(config)), + access_key: config.access_key.clone(), ..Default::default() }, ..Default::default() @@ -542,7 +543,7 @@ pub async fn fetch_block(server_address: &str, block_id: &str, filename: &str) - for (account, _) in &accounts { println!("Fetching transactions of {}", account); - fetch(server_address, + fetch(config, account.as_str(), format!("{}.txns", account).as_str(), false, Some(end_lt), false).await.map_err(err_msg)?; @@ -551,7 +552,7 @@ pub async fn fetch_block(server_address: &str, block_id: &str, filename: &str) - let config_txns_path = format!("{}.txns", CONFIG_ADDR); if !std::path::Path::new(config_txns_path.as_str()).exists() { println!("Fetching transactions of {}", CONFIG_ADDR); - fetch(server_address, + fetch(config, CONFIG_ADDR, config_txns_path.as_str(), false, Some(end_lt), false).await.map_err(err_msg)?; @@ -563,10 +564,10 @@ pub async fn fetch_block(server_address: &str, block_id: &str, filename: &str) - let config_path = format!("config-{}.boc", txnid); if !std::path::Path::new(config_path.as_str()).exists() { println!("Computing config: replaying {} up to {}", acc, txnid); - replay(format!("{}.txns", acc).as_str(), - config_txns_path.as_str(), txnid, - false, None, - || Ok(()), DUMP_CONFIG, None).await.map_err(err_msg)?; + replay(format!("{}.txns", acc).as_str(),config_txns_path.as_str(), + txnid, None, || Ok(()), DUMP_CONFIG, + config, false, true + ).await.map_err(err_msg)?; } else { println!("Using pre-computed config {}", config_path); } @@ -574,6 +575,7 @@ pub async fn fetch_block(server_address: &str, block_id: &str, filename: &str) - println!("Pre-replaying block accounts"); let tasks: Vec<_> = accounts.iter().map(|(account, txns)| { let account_filename = account.split(':').last().unwrap_or("").to_owned(); + let _config = config.clone().to_owned(); let txnid = txns[0].0.clone(); tokio::spawn(async move { if !std::path::Path::new(format!("{}-{}.boc", account_filename, txnid).as_str()).exists() { @@ -581,11 +583,12 @@ pub async fn fetch_block(server_address: &str, block_id: &str, filename: &str) - format!("{}.txns", account_filename).as_str(), format!("{}.txns", CONFIG_ADDR).as_str(), &txnid, - false, None, || Ok(()), DUMP_ACCOUNT, - None + &_config, + false, + true, ).await.map_err(err_msg).unwrap(); } }) @@ -638,7 +641,7 @@ struct BlockAccountDescr { } pub async fn fetch_block_command(m: &ArgMatches<'_>, config: &Config) -> Result<(), String> { - fetch_block(config.url.as_str(), + fetch_block(config, m.value_of("BLOCKID").ok_or("Missing block id")?, m.value_of("OUTPUT").ok_or("Missing output filename")? ).await.map_err(|e| e.to_string())?; @@ -646,7 +649,7 @@ pub async fn fetch_block_command(m: &ArgMatches<'_>, config: &Config) -> Result< } pub async fn fetch_command(m: &ArgMatches<'_>, config: &Config) -> Result<(), String> { - fetch(config.url.as_str(), + fetch(config, m.value_of("ADDRESS").ok_or("Missing account address")?, m.value_of("OUTPUT").ok_or("Missing output filename")?, false, @@ -662,14 +665,15 @@ pub async fn fetch_command(m: &ArgMatches<'_>, config: &Config) -> Result<(), St } pub async fn replay_command(m: &ArgMatches<'_>, cli_config: &Config) -> Result<(), String> { - let (config_txns, cli_config) = if m.is_present("CURRENT_CONFIG") { - ("", Some(cli_config)) + let (config_txns, load_config) = if m.is_present("CURRENT_CONFIG") { + ("", true) } else { - (m.value_of("CONFIG_TXNS").ok_or("Missing config txns filename")?, None) + (m.value_of("CONFIG_TXNS").ok_or("Missing config txns filename")?, false) }; let _ = replay(m.value_of("INPUT_TXNS").ok_or("Missing input txns filename")?, config_txns, m.value_of("TXNID").ok_or("Missing final txn id")?, - false, None, ||{Ok(())}, DUMP_ALL, cli_config + None, ||{Ok(())}, DUMP_ALL, cli_config, + load_config, !m.is_present("IGNORE_HASHES") ).await?; Ok(()) } diff --git a/src/run.rs b/src/run.rs index 7d415943..87a7bf57 100644 --- a/src/run.rs +++ b/src/run.rs @@ -103,18 +103,16 @@ async fn run( create_client_local()? } }; - let abi = std::fs::read_to_string(abi_path.clone()) - .map_err(|e| format!("failed to read ABI file: {}", e.to_string()))?; + let abi = load_abi(&abi_path).await?; let params = if is_alternative { - unpack_alternative_params(matches, &abi, method, config)? + unpack_alternative_params(matches, &abi_path, method, config).await? } else { matches.value_of("PARAMS").map(|s| s.to_owned()) }; let params = Some(load_params(params.unwrap().as_ref())?); - let abi = load_abi(&abi)?; let now = now()?; let expire_at = config.lifetime + now; let header = FunctionHeader { @@ -152,8 +150,8 @@ async fn run( if config.is_json { let e = format!("{:#}", result.clone().err().unwrap()); - let err: serde_json::Value = serde_json::from_str(&e) - .unwrap_or(serde_json::Value::String(e)); + let err: Value = serde_json::from_str(&e) + .unwrap_or(Value::String(e)); let res = json!({"Error": err}); println!("{}", serde_json::to_string_pretty(&res) .unwrap_or("{{ \"JSON serialization error\" }}".to_string())); diff --git a/src/voting.rs b/src/voting.rs index 81044165..d96ecdeb 100644 --- a/src/voting.rs +++ b/src/voting.rs @@ -41,7 +41,7 @@ pub async fn create_proposal( message::generate_message( config, addr, - MSIG_ABI.to_string(), + MSIG_ABI, "submitTransaction", ¶ms, keys, @@ -53,7 +53,7 @@ pub async fn create_proposal( call::call_contract( config, addr, - MSIG_ABI.to_string(), + MSIG_ABI, "submitTransaction", ¶ms, keys, @@ -82,7 +82,7 @@ pub async fn vote( message::generate_message( config, addr, - MSIG_ABI.to_string(), + MSIG_ABI, "confirmTransaction", ¶ms, keys, @@ -94,7 +94,7 @@ pub async fn vote( call::call_contract( config, addr, - MSIG_ABI.to_string(), + MSIG_ABI, "confirmTransaction", ¶ms, keys, @@ -114,7 +114,7 @@ pub async fn decode_proposal( let result = call::call_contract_with_result( config, addr, - MSIG_ABI.to_string(), + MSIG_ABI, "getTransactions", "{}", None, diff --git a/tests/config_contract.saved b/tests/config_contract.saved new file mode 100644 index 00000000..49460366 Binary files /dev/null and b/tests/config_contract.saved differ diff --git a/tests/samples/SafeMultisigWallet_msg.boc b/tests/samples/SafeMultisigWallet_msg.boc new file mode 100644 index 00000000..0e8c6636 Binary files /dev/null and b/tests/samples/SafeMultisigWallet_msg.boc differ diff --git a/tests/test_cli.rs b/tests/test_cli.rs index 2c41e3c1..16d3cd01 100644 --- a/tests/test_cli.rs +++ b/tests/test_cli.rs @@ -4,6 +4,7 @@ use std::env; use std::time::Duration; use std::thread::sleep; use std::fs; +use serde_json::{json, Value}; mod common; use common::{BIN_NAME, NETWORK, giver_v2, grep_address, set_config, GIVER_V2_ABI, @@ -13,10 +14,12 @@ use common::{BIN_NAME, NETWORK, giver_v2, grep_address, set_config, GIVER_V2_ABI const DEPOOL_ABI: &str = "tests/samples/fakeDepool.abi.json"; const DEPOOL_TVC: &str = "tests/samples/fakeDepool.tvc"; const SAFEMSIG_ABI: &str = "tests/samples/SafeMultisigWallet.abi.json"; +const SAFEMSIG_ABI_LINK: &str = "https://raw.githubusercontent.com/tonlabs/ton-labs-contracts/master/solidity/safemultisig/SafeMultisigWallet.abi.json"; const SAFEMSIG_TVC: &str = "tests/samples/SafeMultisigWallet.tvc"; const SAFEMSIG_SEED: &str = "blanket time net universe ketchup maid way poem scatter blur limit drill"; const SAFEMSIG_ADDR: &str = "0:d5f5cfc4b52d2eb1bd9d3a8e51707872c7ce0c174facddd0e06ae5ffd17d2fcd"; const SAFEMSIG_CONSTR_ARG: &str = r#"{"owners":["0xc8bd66f90d61f7e1e1a6151a0dbe9d8640666920d8c0cf399cbfb72e089d2e41"],"reqConfirms":1}"#; +const SAVED_CONFIG: &str = "tests/config_contract.saved"; fn now_ms() -> u64 { chrono::prelude::Utc::now().timestamp_millis() as u64 @@ -197,7 +200,7 @@ fn test_config_aliases() -> Result<(), Box> { .arg("--addr") .arg("0:ece57bcc6c530283becbbd8a3b24d3c5987cdddc3c8b7b33be6e4a6312490415") .arg("--abi") - .arg("tests/samples/SafeMultisigWallet.abi.json") + .arg(SAFEMSIG_ABI) .arg("--keys") .arg("tests/deploy_test.key"); cmd.assert() @@ -482,6 +485,25 @@ fn test_fee() -> Result<(), Box> { .stdout(predicate::str::contains(r#" "out_msgs_fwd_fee":"#)) .stdout(predicate::str::contains(r#" "total_account_fees":"#)) .stdout(predicate::str::contains(r#" "total_output":"#)); + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("fee") + .arg("deploy") + .arg(SAFEMSIG_TVC) + .arg(SAFEMSIG_CONSTR_ARG) + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("--sign") + .arg(key_path) + .assert() + .success() + .stdout(predicate::str::contains(r#" "in_msg_fwd_fee":"#)) + .stdout(predicate::str::contains(r#" "storage_fee":"#)) + .stdout(predicate::str::contains(r#" "gas_fee":"#)) + .stdout(predicate::str::contains(r#" "out_msgs_fwd_fee":"#)) + .stdout(predicate::str::contains(r#" "total_account_fees":"#)) + .stdout(predicate::str::contains(r#" "total_output":"#)); + fs::remove_file(key_path)?; Ok(()) } @@ -1205,6 +1227,22 @@ fn test_decode_msg() -> Result<(), Box> { .stdout(predicate::str::contains("value")) .stdout(predicate::str::contains("bounce")); + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--json") + .arg("decode") + .arg("msg") + .arg("tests/samples/SafeMultisigWallet_msg.boc") + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .assert() + .success() + .stdout(predicate::str::contains("sendTransaction")) + .stdout(predicate::str::contains("dest")) + .stdout(predicate::str::contains("value")) + .stdout(predicate::str::contains("bounce")) + .stdout(predicate::str::contains("BodyCall")) + .stdout(predicate::str::contains("sendTransaction")); + let mut cmd = Command::cargo_bin(BIN_NAME)?; cmd.arg("--json") .arg("decode") @@ -1249,6 +1287,20 @@ fn test_decode_body() -> Result<(), Box> { .stdout(predicate::str::contains("value")) .stdout(predicate::str::contains("bounce")); + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--json") + .arg("decode") + .arg("body") + .arg("te6ccgEBAwEArAAB4cfPwTCZRs1P3cBAsaxZfZctRcTN28wfuL5Z5x+MxMXaLuXENl1xXKUQtidlfmHDQU2p3mQ3n7ctv3ojqkQKmob+LISc2FObbAjXunMPMknNucIYcllCCxgD2Gz1UM78qkAAAGDf5wCoWMzGdZM7mRsgAQFlgBCCUR2nars5tfUA0A/gVBXBgtNUvb/RFPE0yQSFLq1SgAAAAAAAAAAAAAAAAAAw1AAYAgAA") + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK); + cmd.assert() + .success() + .stdout(predicate::str::contains("sendTransaction")) + .stdout(predicate::str::contains("dest")) + .stdout(predicate::str::contains("value")) + .stdout(predicate::str::contains("bounce")); + let mut cmd = Command::cargo_bin(BIN_NAME)?; cmd.arg("--json").arg("decode") .arg("body").arg("te6ccgEBAgEAlgAB4ddyAENPhARLqvYWfcfwyY4fDOfGj88sVFpJjVp9Rh4QN6iL06hBowkex5kc8haTCwWTnugx1OKTuxOumBzdGwLCSzzna0XhE6urkzQv0XbzKbLpZicIiuqBenAdx6nbCkAAAGCSbtgxWLjxbUl77IIgAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGU=") @@ -1942,6 +1994,20 @@ fn test_decode_tvc() -> Result<(), Box> { .stdout(predicate::str::contains(r#""__pubkey": "0xe8b1d839abe27b2abb9d4a2943a9143a9c7e2ae06799bd24dec1d7a8891ae5dd","#)) .stdout(predicate::str::contains(r#" "a": "I like it.","#)); + let abi_str = fs::read_to_string("tests/test_abi_v2.1.abi.json")?; + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("decode") + .arg("account") + .arg("data") + .arg("--abi") + .arg(abi_str) + .arg("--tvc") + .arg("tests/decode_fields.tvc") + .assert() + .success() + .stdout(predicate::str::contains(r#""__pubkey": "0xe8b1d839abe27b2abb9d4a2943a9143a9c7e2ae06799bd24dec1d7a8891ae5dd","#)) + .stdout(predicate::str::contains(r#" "a": "I like it.","#)); + let boc_path = "tests/account.boc"; let mut cmd = Command::cargo_bin(BIN_NAME)?; cmd.arg("decode") @@ -2894,3 +2960,248 @@ fn test_override_url() -> Result<(), Box> { fs::remove_file(cli_config)?; Ok(()) } + +#[test] +fn test_alternative_paths() -> Result<(), Box> { + let key_path = "link_path.key.json"; + let config_path = "link_path.config"; + set_config( + &["--url"], + &[&*NETWORK], + Some(config_path) + )?; + + let address = deploy_contract(key_path, SAFEMSIG_TVC, SAFEMSIG_ABI_LINK, SAFEMSIG_CONSTR_ARG)?; + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("run") + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg(address.clone()) + .arg("getParameters") + .arg("{}"); + cmd.assert() + .success() + .stdout(predicate::str::contains("maxQueuedTransactions\": \"5")); + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("call") + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("--sign") + .arg(SAFEMSIG_SEED) + .arg(&address) + .arg("sendTransaction") + .arg(format!(r#"{{"dest":"{}","value":1000000000,"bounce":true,"flags":1,"payload":""}}"#, &address)); + cmd.assert() + .success(); + + set_config( + &["--abi"], + &[&*SAFEMSIG_ABI_LINK], + Some(config_path) + )?; + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("runx") + .arg("--addr") + .arg(address.clone()) + .arg("-m") + .arg("getParameters"); + cmd.assert() + .success() + .stdout(predicate::str::contains("maxQueuedTransactions\": \"5")); + + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("callx") + .arg("--keys") + .arg(SAFEMSIG_SEED) + .arg("--addr") + .arg(&address) + .arg("-m") + .arg("sendTransaction") + .arg("--dest") + .arg(&address) + .arg("--value") + .arg("1000000000") + .arg("--bounce") + .arg("true") + .arg("--flags") + .arg("1") + .arg("--payload") + .arg(""); + cmd.assert() + .success(); + + let abi_str = fs::read_to_string(SAFEMSIG_ABI)?; + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("runx") + .arg("--addr") + .arg(address.clone()) + .arg("-m") + .arg("getParameters") + .arg("--abi") + .arg(abi_str); + cmd.assert() + .success() + .stdout(predicate::str::contains("maxQueuedTransactions\": \"5")); + + let key_path2 = "link_path2.key.json"; + let address = generate_key_and_address(key_path2, SAFEMSIG_TVC, SAFEMSIG_ABI_LINK)?; + giver_v2(&address); + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("deployx") + .arg(SAFEMSIG_TVC) + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("--keys") + .arg(key_path2) + .arg(SAFEMSIG_CONSTR_ARG); + cmd.assert() + .success() + .stdout(predicate::str::contains(&address)) + .stdout(predicate::str::contains("Transaction succeeded.")); + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + let out = cmd.arg("-j") + .arg("--config") + .arg(config_path) + .arg("message") + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("--sign") + .arg(SAFEMSIG_SEED) + .arg(&address) + .arg("sendTransaction") + .arg(format!(r#"{{"dest":"{}","value":1000000000,"bounce":true,"flags":1,"payload":""}}"#, &address)) + .output() + .expect("Failed to generate message"); + let result = String::from_utf8_lossy(&out.stdout).to_string(); + let data = serde_json::from_str::(&result).unwrap_or(json!({})); + let message = data["Message"].to_string().trim_end_matches('\"') + .trim_start_matches('\"').to_string(); + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("send") + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg(message); + cmd.assert() + .success(); + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("body") + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("sendTransaction") + .arg(format!(r#"{{"dest":"{}","value":1000000000,"bounce":true,"flags":1,"payload":""}}"#, &address)); + cmd.assert() + .success(); + + let tvc_path = "msig.tvc2"; + fs::copy(SAFEMSIG_TVC, tvc_path)?; + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("genaddr") + .arg(tvc_path) + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("--save") + .arg("--setkey") + .arg("crater skill hazard catalog over recycle drum tragic thunder crouch lunch supply") + .assert() + .success(); + + let trace_path = "test_trace.log"; + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("debug") + .arg("deploy") + .arg("--init_balance") + .arg("--config") + .arg(SAVED_CONFIG) + .arg("--output") + .arg(trace_path) + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("--keys") + .arg("crater skill hazard catalog over recycle drum tragic thunder crouch lunch supply") + .arg(SAFEMSIG_TVC) + .arg(SAFEMSIG_CONSTR_ARG); + cmd.assert() + .success() + .stdout(predicate::str::contains("Log saved to ")); + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("debug") + .arg("run") + .arg("--addr") + .arg(address.clone()) + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("--config") + .arg(SAVED_CONFIG) + .arg("--output") + .arg(trace_path) + .arg("-m") + .arg("getParameters"); + cmd.assert() + .success() + .stdout(predicate::str::contains("Log saved to ")); + + + let mut cmd = Command::cargo_bin(BIN_NAME)?; + cmd.arg("--config") + .arg(config_path) + .arg("debug") + .arg("call") + .arg("--keys") + .arg(SAFEMSIG_SEED) + .arg("--addr") + .arg(&address) + .arg("--abi") + .arg(SAFEMSIG_ABI_LINK) + .arg("--output") + .arg(trace_path) + .arg("-m") + .arg("sendTransaction") + .arg("--") + .arg("--dest") + .arg(&address) + .arg("--value") + .arg("1000000000") + .arg("--bounce") + .arg("true") + .arg("--flags") + .arg("1") + .arg("--payload") + .arg(""); + cmd.assert() + .success() + .stdout(predicate::str::contains("Log saved to ")); + + fs::remove_file(tvc_path)?; + fs::remove_file(key_path)?; + fs::remove_file(key_path2)?; + fs::remove_file(config_path)?; + + Ok(()) +}