diff --git a/programs/eosc/CMakeLists.txt b/programs/eosc/CMakeLists.txt index e867d77398d0694e7a4f1036a17d4177f6733825..172def2d183a387b01aa03a3f937262461e8682b 100644 --- a/programs/eosc/CMakeLists.txt +++ b/programs/eosc/CMakeLists.txt @@ -1,4 +1,4 @@ -add_executable( eosc main.cpp httpc.cpp ) +add_executable( eosc main.cpp httpc.cpp help_text.cpp ) if( UNIX AND NOT APPLE ) set(rt_library rt ) endif() diff --git a/programs/eosc/help_text.cpp b/programs/eosc/help_text.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d2917fe9430b24e01f5f053500185e977b51b9dd --- /dev/null +++ b/programs/eosc/help_text.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2017, Respective Authors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "help_text.hpp" +#include +#include + +const char* transaction_help_text_header = 1 + R"text( +An error occurred while submitting the transaction for this command! +)text"; + +const char* duplicate_transaction_help_text = 1 + R"text( +The transaction is a duplicate of one already pushed to the producers. If this +is an intentionally repeated transaction there are a few ways to resolve the +issue: + - wait for the next block + - combine duplicate transactions into a single transaction + - adjust the expiration time using the `--expiration ` option + - use the `--force-unique` option to add additional nonce data + Please note, this will consume more bandwidth than the base transaction +)text"; + +const char* missing_sigs_help_text = 1 + R"text( +The transaction requires permissions that could not be authorized by the wallet. +Missing authrizations: + - ${1}@${2} + +Please make sure the proper keys are imported into an unlocked wallet and try again! +)text"; + +const char* unknown_account_help_text = 1 + R"text( +The transaction references an account which does not exist. +Unknown accounts: + - ${1} + +Please check the account names and try again! +)text"; + +const char* missing_abi_help_text = 1 + R"text( +The ABI for action "${2}" on code account "${1}" is unknown. +The payload cannot be automatically serialized. + +You can push an arbitrary transaction using the 'push transaction' subcommand +)text"; + +const char* unknown_wallet_help_text = 1 + R"text( +Unable to find a wallet named "${1}", are you sure you typed the name correctly? +)text"; + +const char* bad_wallet_password_help_text = 1 + R"text( + +Invalid password for wallet named "${1}" +)text"; + +const char* locked_wallet_help_text = 1 + R"text( +The wallet named "${1}" is locked. Please unlock it and try again. +)text"; + +const std::vector>> error_help_text { + {"Error\n: 3030011", {transaction_help_text_header, duplicate_transaction_help_text}}, + {"Error\n: 3030002[^\\x00]*Transaction declares authority.*account\":\"([^\"]*)\",\"permission\":\"([^\"]*)\"", {transaction_help_text_header, missing_sigs_help_text}}, + {"Account not found: ([\\S]*)", {transaction_help_text_header, unknown_account_help_text}}, + {"Error\n: 303", {transaction_help_text_header}}, + {"unknown key[^\\x00]*abi_json_to_bin.*code\":\"([^\"]*)\".*action\":\"([^\"]*)\"", {missing_abi_help_text}}, + {"Unable to open file[^\\x00]*wallet/open.*postdata\":\"([^\"]*)\"", {unknown_wallet_help_text}}, + {"AES error[^\\x00]*wallet/unlock.*postdata\":\\[\"([^\"]*)\"", {bad_wallet_password_help_text}}, + {"Wallet is locked: ([\\S]*)", {locked_wallet_help_text}}, +}; + +auto smatch_to_variant(const std::smatch& smatch) { + auto result = fc::mutable_variant_object(); + for(size_t index = 0; index < smatch.size(); index++) { + auto name = boost::lexical_cast(index); + if (smatch[index].matched) { + result = result(name, smatch.str(index)); + } else { + result = result(name, ""); + } + } + + return result; +}; + +namespace eos { namespace client { namespace help { + +bool print_help_text(const fc::exception& e) { + bool result = false; + auto detail_str = e.to_detail_string(); + try { + for (const auto& candidate : error_help_text) { + auto expr = std::regex {candidate.first}; + std::smatch matches; + if (std::regex_search(detail_str, matches, expr)) { + auto args = smatch_to_variant(matches); + for (const auto& msg: candidate.second) { + std::cerr << fc::format_string(msg, args) << std::endl; + } + result = true; + break; + } + } + } catch (const std::regex_error& e ) { + std::cerr << "Error locating help text: "<< e.code() << " " << e.what() << std::endl; + } + + return result; +} + +}}} \ No newline at end of file diff --git a/programs/eosc/help_text.hpp b/programs/eosc/help_text.hpp new file mode 100644 index 0000000000000000000000000000000000000000..64b0f0fb484ea461efd9ddabef387eeb67ca4957 --- /dev/null +++ b/programs/eosc/help_text.hpp @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2017, Respective Authors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include + +namespace eos { namespace client { namespace help { + bool print_help_text(const fc::exception& e); +}}} \ No newline at end of file diff --git a/programs/eosc/main.cpp b/programs/eosc/main.cpp index 153dcb34711b2b1ed24451d574ab487511a262e3..7d2f9469b857f14f0a2664513aaa10a995b4733a 100644 --- a/programs/eosc/main.cpp +++ b/programs/eosc/main.cpp @@ -89,11 +89,15 @@ Options: #include #include "CLI11.hpp" +#include "help_text.hpp" + +#define format_output(format, ...) fc::format_string(format, fc::mutable_variant_object() __VA_ARGS__ ) using namespace std; using namespace eos; using namespace eos::chain; using namespace eos::utilities; +using namespace eos::client::help; string program = "eosc"; string host = "localhost"; @@ -132,7 +136,6 @@ const string wallet_unlock = wallet_func_base + "/unlock"; const string wallet_import_key = wallet_func_base + "/import_key"; const string wallet_sign_trx = wallet_func_base + "/sign_transaction"; - inline std::vector sort_names( std::vector&& names ) { std::sort( names.begin(), names.end() ); auto itr = std::unique( names.begin(), names.end() ); @@ -172,6 +175,30 @@ vector assemble_wast( const std::string& wast ) { } } +auto tx_expiration = fc::microseconds(100); +bool tx_force_unique = false; +void add_standard_transaction_options(CLI::App* cmd) { + CLI::callback_t parse_exipration = [](CLI::results_t res) -> bool { + double value_ms; + if (res.size() == 0 || !CLI::detail::lexical_cast(res[0], value_ms)) { + return false; + } + + tx_expiration = fc::microseconds(static_cast(value_ms * 1000.0)); + return true; + }; + + cmd->add_option("-x,--expiration", parse_exipration, "set the time in milliseconds before a transaction expires, defaults to 0.1ms"); + cmd->add_flag("-f,--force-unique", tx_force_unique, "force the transaction to be unique. this will consume extra bandwidth and remove any protections against accidently issuing the same transaction multiple times"); +} + +std::string generate_nonce_string() { + return std::to_string(fc::time_point::now().time_since_epoch().count() % 1000000); +} + +types::Message generate_nonce() { + return Message(N(eos),{}, N(nonce), generate_nonce_string()); +} vector get_account_permissions(const vector& permissions) { auto fixedPermissions = permissions | boost::adaptors::transformed([](const string& p) { @@ -218,7 +245,7 @@ void sign_transaction(SignedTransaction& trx) { fc::variant push_transaction( SignedTransaction& trx, bool sign ) { auto info = get_info(); - trx.expiration = info.head_block_time + 100; //chain.head_block_time() + 100; + trx.expiration = info.head_block_time + tx_expiration; //chain.head_block_time() + 100; transaction_set_reference_block(trx, info.head_block_id); boost::sort( trx.scope ); @@ -253,6 +280,9 @@ int main( int argc, char** argv ) { app.add_option( "--wallet-host", wallet_host, "the host where eos-walletd is running", true ); app.add_option( "--wallet-port", wallet_port, "the port where eos-walletd is running", true ); + bool verbose_errors = false; + app.add_flag( "-v,--verbose", verbose_errors, "output verbose messages on error"); + // Create subcommand auto create = app.add_subcommand("create", "Create various items, on and off the blockchain", false); create->require_subcommand(); @@ -548,14 +578,28 @@ int main( int argc, char** argv ) { transfer->add_option("amount", amount, "The amount of EOS to send")->required(); transfer->add_option("memo", memo, "The memo for the transfer"); transfer->add_flag("-s,--skip-sign", skip_sign, "Specify that unlocked wallet keys should not be used to sign transaction"); + add_standard_transaction_options(transfer); transfer->set_callback([&] { SignedTransaction trx; trx.scope = sort_names({sender,recipient}); + + if (tx_force_unique) { + if (memo.size() == 0) { + // use the memo to add a nonce + memo = generate_nonce_string(); + } else { + // add a nonce message + transaction_emplace_message(trx, generate_nonce()); + } + } + transaction_emplace_message(trx, config::EosContractName, vector{{sender,"active"}}, "transfer", types::transfer{sender, recipient, amount, memo}); + + auto info = get_info(); - trx.expiration = info.head_block_time + 100; //chain.head_block_time() + 100; + trx.expiration = info.head_block_time + tx_expiration; //chain.head_block_time() + 100; transaction_set_reference_block(trx, info.head_block_id); if (!skip_sign) { sign_transaction(trx); @@ -689,7 +733,7 @@ int main( int argc, char** argv ) { types::newaccount{creator, newaccount, owner_auth, active_auth, recovery_auth, deposit}); - trx.expiration = info.head_block_time + 100; + trx.expiration = info.head_block_time + tx_expiration; transaction_set_reference_block(trx, info.head_block_id); batch.emplace_back(trx); } @@ -720,7 +764,7 @@ int main( int argc, char** argv ) { transaction_emplace_message(trx, config::EosContractName, vector{{sender,"active"}}, "transfer", types::transfer{sender, recipient, amount, memo}); - trx.expiration = info.head_block_time + 100; + trx.expiration = info.head_block_time + tx_expiration; transaction_set_reference_block(trx, info.head_block_id); batch.emplace_back(trx); @@ -757,7 +801,7 @@ int main( int argc, char** argv ) { transaction_emplace_message(trx, config::EosContractName, vector{{sender,"active"}}, "transfer", types::transfer{sender, recipient, amount, memo}); - trx.expiration = info.head_block_time + 100; + trx.expiration = info.head_block_time + tx_expiration; transaction_set_reference_block(trx, info.head_block_id); batch.emplace_back(trx); @@ -806,6 +850,11 @@ int main( int argc, char** argv ) { SignedTransaction trx; transaction_emplace_serialized_message(trx, contract, action, accountPermissions, result.get_object()["binargs"].as()); + + if (tx_force_unique) { + transaction_emplace_message(trx, generate_nonce()); + } + for( const auto& scope : scopes ) { vector subscopes; boost::split( subscopes, scope, boost::is_any_of( ", :" ) ); @@ -841,14 +890,21 @@ int main( int argc, char** argv ) { auto errorString = e.to_detail_string(); if (errorString.find("Connection refused") != string::npos) { if (errorString.find(fc::json::to_string(port)) != string::npos) { - elog("Failed to connect to eosd at ${ip}:${port}; is eosd running?", ("ip", host)("port", port)); + std::cerr << format_output("Failed to connect to eosd at ${ip}:${port}; is eosd running?", ("ip", host)("port", port)) << std::endl; } else if (errorString.find(fc::json::to_string(wallet_port)) != string::npos) { - elog("Failed to connect to eos-walletd at ${ip}:${port}; is eos-walletd running?", ("ip", wallet_host)("port", wallet_port)); + std::cerr << format_output("Failed to connect to eos-walletd at ${ip}:${port}; is eos-walletd running?", ("ip", wallet_host)("port", wallet_port)) << std::endl; } else { - elog("Failed to connect with error: ${e}", ("e", e.to_detail_string())); + std::cerr << format_output("Failed to connect") << std::endl; + } + + if (verbose_errors) { + elog("connect error: ${e}", ("e", errorString)); } } else { - elog("Failed with error: ${e}", ("e", e.to_detail_string())); + // attempt to extract the error code if one is present + if (!print_help_text(e) || verbose_errors) { + elog("Failed with error: ${e}", ("e", e.to_detail_string())); + } } return 1; }