diff --git a/CHANGELOG.md b/CHANGELOG.md index 5be9d219..02d6142c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,4 +55,11 @@ This fixes affects all debots invoking other debots (e.g. depool debot, mludi de ## v0.11.5 ### Improvements -- `tonos-cli` now provides several different endpoints for main.ton.dev and net.ton.dev in order to improve reliability. \ No newline at end of file +- `tonos-cli` now provides several different endpoints for main.ton.dev and net.ton.dev in order to improve reliability. + +## v0.12.0 + +### New feature +- tonos-cli now can execute the transaction locally for deploy and call commands before executing + it onchain. If local execution fails, onchain execution is not performed. Local run can be + enabled by setting the flag `local_run` in the tonos-cli config. diff --git a/Cargo.toml b/Cargo.toml index 3062706c..3b61f837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" license = "Apache-2.0" keywords = ["TON", "SDK", "smart contract", "tonlabs", "solidity"] edition = "2018" -version = "0.11.6" +version = "0.12.0" [dependencies] async-trait = "0.1.42" diff --git a/README.md b/README.md index 424c0aa7..cd2a0ead 100644 --- a/README.md +++ b/README.md @@ -165,19 +165,22 @@ in the most part of subcommands. List of possible parameters: - --abi File with contract ABI. - --addr Contract address. - --depool_fee Value added to message sent to depool to cover it's fees (change will be returned). - --keys File with keypair. - --lifetime Period of time in seconds while message is valid. - --no-answer FLag whether to wait for depool answer when calling a depool function. - --retries Number of attempts to call smart contract function if previous attempt was - unsuccessful. - --timeout Contract call timeout in ms. - --url Url to connect. - --wallet Multisig wallet address. Used in commands which send internal messages through - multisig wallets. - --wc Workchain id. + --abi File with contract ABI. + --addr Contract address. + --depool_fee Value added to message sent to depool to cover it's fees (change will be + returned). + --keys File with keypair. + --lifetime Period of time in seconds while message is valid. + --local_run Enable preliminary local run before deploy and call commands. + --no-answer FLag whether to wait for depool answer when calling a depool function. + --retries Number of attempts to call smart contract function if previous attempt was + unsuccessful. + --timeout Contract call timeout in ms. + --url Url to connect. + --delimiters Use delimiters while printing account balance + --wallet Multisig wallet address. Used in commands which send internal messages through + multisig wallets. + --wc Workchain id. Also user can clear one or all parameters from the config file (it will set the default value for them if it is possible. diff --git a/src/account.rs b/src/account.rs index f93472b6..cbb362b9 100644 --- a/src/account.rs +++ b/src/account.rs @@ -21,6 +21,7 @@ const ACCOUNT_FIELDS: &str = r#" last_paid last_trans_lt data + code_hash "#; pub async fn get_account(conf: Config, addr: &str) -> Result<(), String> { @@ -62,6 +63,7 @@ pub async fn get_account(conf: Config, addr: &str) -> Result<(), String> { } else { println!(" \"data(boc)\": \"null\""); } + println!(" code_hash: {}", acc["code_hash"].as_str().unwrap_or("null")); } else { println!(" \"acc_type\": \"{}\"", acc_type); } @@ -100,6 +102,7 @@ pub async fn get_account(conf: Config, addr: &str) -> Result<(), String> { } else { println!("data(boc): null"); } + println!("code_hash: {}", acc["code_hash"].as_str().unwrap_or("null")); } else { println!("Account does not exist."); } @@ -108,4 +111,4 @@ pub async fn get_account(conf: Config, addr: &str) -> Result<(), String> { } } Ok(()) -} \ No newline at end of file +} diff --git a/src/call.rs b/src/call.rs index 91237f3a..039e8dde 100644 --- a/src/call.rs +++ b/src/call.rs @@ -33,7 +33,7 @@ use ton_client::processing::{ wait_for_transaction, send_message, }; -use ton_client::tvm::{run_tvm, run_get, ParamsOfRunTvm, ParamsOfRunGet}; +use ton_client::tvm::{run_tvm, run_get, ParamsOfRunTvm, ParamsOfRunGet, run_executor, ParamsOfRunExecutor, AccountForExecutor}; use ton_client::error::ClientError; pub struct EncodedMessage { @@ -245,6 +245,32 @@ async fn query_account_boc(ton: TonClient, addr: &str) -> Result Ok(boc.unwrap().to_owned()) } +pub async fn emulate_localy( + ton: TonClient, + addr: &str, + msg: String, +) -> Result<(), String> { + let state_boc = query_account_boc(ton.clone(), addr).await?; + + let res = run_executor( + ton.clone(), + ParamsOfRunExecutor { + message: msg.clone(), + account: AccountForExecutor::Account { + boc: state_boc, + unlimited_balance: None, + }, + ..Default::default() + }, + ) + .await; + + if res.is_err() { + return Err(format!("{:#}", res.err().unwrap())); + } + Ok(()) +} + async fn send_message_and_wait( ton: TonClient, addr: &str, @@ -353,7 +379,7 @@ pub async fn call_contract_with_result( if !conf.is_json { print_encoded_message(&msg); } - + let mut retry: bool = true; let error_handler = |err: ClientError| { // obtaining error code @@ -361,25 +387,30 @@ pub async fn call_contract_with_result( // but if it was simulated locally and local exit code is not zero, // we ignore previous exit code because it means we shouldn't make a retry. if !err.data["exit_code"].is_null() { - if err.data["exit_code"].as_i64().unwrap() != 0 { + if err.data["exit_code"].as_i64().unwrap_or(-1) != 0 { retry = false; } } // There is also another way how SDK can print local run results. let local_error = err.data["local_error"]["data"]["exit_code"].clone(); if !local_error.is_null() { - if local_error.as_i64().unwrap() != 0 { + if local_error.as_i64().unwrap_or(-1) != 0 { retry = false; } } - // if error code was 4XX then don't perform a retry. - let code = (code / 100) as u32 % 10; - if code == 4 { + // if error code was 4XX then don't perform a retry. Also if error was 508, + // it means that message could have been delivered after timeout and for not + // to cause double call we shouldn't perform a retry. + if (((code / 100) as u32 % 10) == 4) || (code == 508) { retry = false; } }; + + if !local && conf.local_run { + emulate_localy(ton.clone(), addr, msg.message.clone()).await?; + } let result = send_message_and_wait(ton.clone(), addr, abi.clone(), msg.message, local, conf.clone(), error_handler).await; - + if result.is_ok() { return result; } diff --git a/src/config.rs b/src/config.rs index e1cffd05..fd19203a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -67,6 +67,8 @@ pub struct Config { pub no_answer: bool, #[serde(default = "default_false")] pub use_delimiters: bool, + #[serde(default = "default_false")] + pub local_run: bool, } impl Config { @@ -85,6 +87,7 @@ impl Config { lifetime: default_lifetime(), no_answer: default_true(), use_delimiters: default_false(), + local_run: default_false(), } } @@ -110,6 +113,7 @@ pub fn clear_config( lifetime: bool, no_answer: bool, use_delimiters: bool, + local_run: bool, ) -> Result<(), String> { if url { conf.url = default_url(); @@ -147,9 +151,12 @@ pub fn clear_config( if use_delimiters { conf.use_delimiters = default_false(); } + if local_run { + conf.local_run = default_false(); + } if (url || addr || wallet || abi || keys || retries || timeout || wc || depool_fee || lifetime - || no_answer || use_delimiters) == false { + || no_answer || use_delimiters || local_run) == false { conf = Config::new(); } let conf_str = serde_json::to_string(&conf) @@ -175,6 +182,7 @@ pub fn set_config( lifetime: Option<&str>, no_answer: Option<&str>, use_delimiters: Option<&str>, + local_run: Option<&str>, ) -> Result<(), String> { if let Some(s) = url { conf.url = s.to_string(); @@ -222,6 +230,10 @@ pub fn set_config( conf.use_delimiters = use_delimiters.parse::() .map_err(|e| format!(r#"failed to parse "use_delimiters": {}"#, e))?; } + if let Some(local_run) = local_run { + conf.local_run = local_run.parse::() + .map_err(|e| format!(r#"failed to parse "local_run": {}"#, e))?; + } let conf_str = serde_json::to_string(&conf) .map_err(|_| "failed to serialize config object".to_string())?; diff --git a/src/deploy.rs b/src/deploy.rs index 1373fa60..b65231f6 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -13,7 +13,7 @@ use crate::helpers::{create_client_verbose, create_client_local, load_abi, calc_acc_address}; use crate::config::Config; use crate::crypto::load_keypair; -use crate::call::{EncodedMessage, display_generated_message}; +use crate::call::{EncodedMessage, display_generated_message, emulate_localy}; use ton_client::processing::{ParamsOfProcessMessage}; use ton_client::abi::{ encode_message, Signer, CallSet, DeploySet, ParamsOfEncodeMessage @@ -33,6 +33,13 @@ pub async fn deploy_contract( let (msg, addr) = prepare_deploy_message(tvc, abi, params, keys_file, wc).await?; + let enc_msg = encode_message(ton.clone(), msg.clone()).await + .map_err(|e| format!("failed to create inbound message: {}", e))?; + + if conf.local_run { + emulate_localy(ton.clone(), addr.as_str(), enc_msg.message).await?; + } + let callback = |_event| { async move { } }; ton_client::processing::process_message( ton.clone(), diff --git a/src/main.rs b/src/main.rs index 519a0922..33060936 100644 --- a/src/main.rs +++ b/src/main.rs @@ -292,6 +292,7 @@ async fn main_internal() -> Result <(), String> { (@arg LIFETIME: --lifetime +takes_value "Period of time in seconds while message is valid.") (arg: no_answer_with_value) (@arg USE_DELIMITERS: --delimiters +takes_value "Use delimiters while printing account balance") + (@arg LOCAL_RUN: --local_run +takes_value "Enable preliminary local run before deploy and call commands.") (@subcommand clear => (@setting AllowLeadingHyphen) (about: "Resets certain default values for options in the config file. Resets all values if used without options.") @@ -307,6 +308,7 @@ async fn main_internal() -> Result <(), String> { (@arg LIFETIME: --lifetime "Period of time in seconds while message is valid.") (arg: no_answer) (@arg USE_DELIMITERS: --delimiters "Use delimiters while printing account balance") + (@arg LOCAL_RUN: --local_run "Enable preliminary local run before deploy and call commands.") ) ) (@subcommand account => @@ -734,7 +736,8 @@ fn config_command(matches: &ArgMatches, config: Config, config_file: String) -> let lifetime = clear_matches.is_present("LIFETIME"); let no_answer = clear_matches.is_present("NO_ANSWER"); let use_delimiters = clear_matches.is_present("USE_DELIMITERS"); - result = clear_config(config, config_file.as_str(), url, address, wallet, abi, keys, wc, retries, timeout, depool_fee, lifetime, no_answer, use_delimiters); + let local_run = clear_matches.is_present("LOCAL_RUN"); + result = clear_config(config, config_file.as_str(), url, address, wallet, abi, keys, wc, retries, timeout, depool_fee, lifetime, no_answer, use_delimiters, local_run); } else { let url = matches.value_of("URL"); let address = matches.value_of("ADDR"); @@ -748,7 +751,8 @@ fn config_command(matches: &ArgMatches, config: Config, config_file: String) -> let lifetime = matches.value_of("LIFETIME"); let no_answer = matches.value_of("NO_ANSWER"); let use_delimiters = matches.value_of("USE_DELIMITERS"); - result = set_config(config, config_file.as_str(), url, address, wallet, abi, keys, wc, retries, timeout, depool_fee, lifetime, no_answer, use_delimiters); + let local_run = matches.value_of("LOCAL_RUN"); + result = set_config(config, config_file.as_str(), url, address, wallet, abi, keys, wc, retries, timeout, depool_fee, lifetime, no_answer, use_delimiters, local_run); } } let config = match Config::from_file(config_file.as_str()) { diff --git a/tests/data.tvc b/tests/data.tvc index 7b828b68..7400bafd 100644 Binary files a/tests/data.tvc and b/tests/data.tvc differ