// Copyright (c) 2014-2018, The Monero Project // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other // materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers /*! * \file simplewallet.cpp * * \brief Source file that defines simple_wallet class. */ #include #include #include #include #include #include #include #include #include #include #include "include_base_utils.h" #include "common/i18n.h" #include "common/command_line.h" #include "common/util.h" #include "common/dns_utils.h" #include "common/base58.h" #include "common/scoped_message_writer.h" #include "cryptonote_protocol/cryptonote_protocol_handler.h" #include "simplewallet.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "storages/http_abstract_invoke.h" #include "rpc/core_rpc_server_commands_defs.h" #include "crypto/crypto.h" // for crypto::secret_key definition #include "mnemonics/electrum-words.h" #include "rapidjson/document.h" #include "common/json_util.h" #include "ringct/rctSigs.h" #include "multisig/multisig.h" #include "wallet/wallet_args.h" #include "version.h" #include #include "wallet/message_store.h" #ifdef WIN32 #include #include #endif #ifdef HAVE_READLINE #include "readline_buffer.h" #endif using namespace std; using namespace epee; using namespace cryptonote; using boost::lexical_cast; namespace po = boost::program_options; typedef cryptonote::simple_wallet sw; #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "wallet.simplewallet" #define EXTENDED_LOGS_FILE "wallet_details.log" #define OUTPUT_EXPORT_FILE_MAGIC "Monero output export\003" #define LOCK_IDLE_SCOPE() \ bool auto_refresh_enabled = m_auto_refresh_enabled.load(std::memory_order_relaxed); \ m_auto_refresh_enabled.store(false, std::memory_order_relaxed); \ /* stop any background refresh, and take over */ \ m_wallet->stop(); \ boost::unique_lock lock(m_idle_mutex); \ m_idle_cond.notify_all(); \ epee::misc_utils::auto_scope_leave_caller scope_exit_handler = epee::misc_utils::create_scope_leave_handler([&](){ \ m_auto_refresh_enabled.store(auto_refresh_enabled, std::memory_order_relaxed); \ }) enum TransferType { TransferOriginal, TransferNew, TransferLocked, }; namespace { const std::array allowed_priority_strings = {{"default", "unimportant", "normal", "elevated", "priority"}}; const auto arg_wallet_file = wallet_args::arg_wallet_file(); const command_line::arg_descriptor arg_generate_new_wallet = {"generate-new-wallet", sw::tr("Generate new wallet and save it to "), ""}; const command_line::arg_descriptor arg_generate_from_device = {"generate-from-device", sw::tr("Generate new wallet from device and save it to "), ""}; const command_line::arg_descriptor arg_generate_from_view_key = {"generate-from-view-key", sw::tr("Generate incoming-only wallet from view key"), ""}; const command_line::arg_descriptor arg_generate_from_spend_key = {"generate-from-spend-key", sw::tr("Generate deterministic wallet from spend key"), ""}; const command_line::arg_descriptor arg_generate_from_keys = {"generate-from-keys", sw::tr("Generate wallet from private keys"), ""}; const command_line::arg_descriptor arg_generate_from_multisig_keys = {"generate-from-multisig-keys", sw::tr("Generate a master wallet from multisig wallet keys"), ""}; const auto arg_generate_from_json = wallet_args::arg_generate_from_json(); const command_line::arg_descriptor arg_mnemonic_language = {"mnemonic-language", sw::tr("Language for mnemonic"), ""}; const command_line::arg_descriptor arg_electrum_seed = {"electrum-seed", sw::tr("Specify Electrum seed for wallet recovery/creation"), ""}; const command_line::arg_descriptor arg_restore_deterministic_wallet = {"restore-deterministic-wallet", sw::tr("Recover wallet using Electrum-style mnemonic seed"), false}; const command_line::arg_descriptor arg_restore_multisig_wallet = {"restore-multisig-wallet", sw::tr("Recover multisig wallet using Electrum-style mnemonic seed"), false}; const command_line::arg_descriptor arg_non_deterministic = {"non-deterministic", sw::tr("Generate non-deterministic view and spend keys"), false}; const command_line::arg_descriptor arg_trusted_daemon = {"trusted-daemon", sw::tr("Enable commands which rely on a trusted daemon"), false}; const command_line::arg_descriptor arg_untrusted_daemon = {"untrusted-daemon", sw::tr("Disable commands which rely on a trusted daemon"), false}; const command_line::arg_descriptor arg_allow_mismatched_daemon_version = {"allow-mismatched-daemon-version", sw::tr("Allow communicating with a daemon that uses a different RPC version"), false}; const command_line::arg_descriptor arg_restore_height = {"restore-height", sw::tr("Restore from specific blockchain height"), 0}; const command_line::arg_descriptor arg_do_not_relay = {"do-not-relay", sw::tr("The newly created transaction will not be relayed to the wownero network"), false}; const command_line::arg_descriptor arg_create_address_file = {"create-address-file", sw::tr("Create an address file for new wallets"), false}; const command_line::arg_descriptor arg_subaddress_lookahead = {"subaddress-lookahead", tools::wallet2::tr("Set subaddress lookahead sizes to :"), ""}; const command_line::arg_descriptor arg_use_english_language_names = {"use-english-language-names", sw::tr("Display English language names"), false}; const command_line::arg_descriptor< std::vector > arg_command = {"command", ""}; #ifdef WIN32 // Translate from CP850 to UTF-8; // std::getline for a Windows console returns a string in CP437 or CP850; as simplewallet, // like all of Monero, is assumed to work internally with UTF-8 throughout, even on Windows // (although only implemented partially), a translation to UTF-8 is needed for input. // // Note that if a program is started inside the MSYS2 shell somebody already translates // console input to UTF-8, but it's not clear how one could detect that in order to avoid // double-translation; this code here thus breaks UTF-8 input within a MSYS2 shell, // unfortunately. // // Note also that input for passwords is NOT translated, to remain compatible with any // passwords containing special characters that predate this switch to UTF-8 support. static std::string cp850_to_utf8(const std::string &cp850_str) { boost::locale::generator gen; gen.locale_cache_enabled(true); std::locale loc = gen("en_US.CP850"); return boost::locale::conv::to_utf(cp850_str, loc); } #endif std::string input_line(const std::string& prompt) { #ifdef HAVE_READLINE rdln::suspend_readline pause_readline; #endif std::cout << prompt; std::string buf; std::getline(std::cin, buf); #ifdef WIN32 buf = cp850_to_utf8(buf); #endif return epee::string_tools::trim(buf); } boost::optional password_prompter(const char *prompt, bool verify) { #ifdef HAVE_READLINE rdln::suspend_readline pause_readline; #endif auto pwd_container = tools::password_container::prompt(verify, prompt); if (!pwd_container) { tools::fail_msg_writer() << tr("failed to read wallet password"); } return pwd_container; } boost::optional default_password_prompter(bool verify) { return password_prompter(verify ? tr("Enter a new password for the wallet") : tr("Wallet password"), verify); } inline std::string interpret_rpc_response(bool ok, const std::string& status) { std::string err; if (ok) { if (status == CORE_RPC_STATUS_BUSY) { err = sw::tr("daemon is busy. Please try again later."); } else if (status != CORE_RPC_STATUS_OK) { err = status; } } else { err = sw::tr("possibly lost connection to daemon"); } return err; } tools::scoped_message_writer success_msg_writer(bool color = false) { return tools::scoped_message_writer(color ? console_color_green : console_color_default, false, std::string(), el::Level::Info); } tools::scoped_message_writer message_writer(epee::console_colors color = epee::console_color_default, bool bright = false) { return tools::scoped_message_writer(color, bright); } tools::scoped_message_writer fail_msg_writer() { return tools::scoped_message_writer(console_color_red, true, sw::tr("Error: "), el::Level::Error); } bool parse_bool(const std::string& s, bool& result) { if (s == "1" || command_line::is_yes(s)) { result = true; return true; } if (s == "0" || command_line::is_no(s)) { result = false; return true; } boost::algorithm::is_iequal ignore_case{}; if (boost::algorithm::equals("true", s, ignore_case) || boost::algorithm::equals(simple_wallet::tr("true"), s, ignore_case)) { result = true; return true; } if (boost::algorithm::equals("false", s, ignore_case) || boost::algorithm::equals(simple_wallet::tr("false"), s, ignore_case)) { result = false; return true; } return false; } template bool parse_bool_and_use(const std::string& s, F func) { bool r; if (parse_bool(s, r)) { func(r); return true; } else { fail_msg_writer() << tr("invalid argument: must be either 0/1, true/false, y/n, yes/no"); return false; } } const struct { const char *name; tools::wallet2::RefreshType refresh_type; } refresh_type_names[] = { { "full", tools::wallet2::RefreshFull }, { "optimize-coinbase", tools::wallet2::RefreshOptimizeCoinbase }, { "optimized-coinbase", tools::wallet2::RefreshOptimizeCoinbase }, { "no-coinbase", tools::wallet2::RefreshNoCoinbase }, { "default", tools::wallet2::RefreshDefault }, }; bool parse_refresh_type(const std::string &s, tools::wallet2::RefreshType &refresh_type) { for (size_t n = 0; n < sizeof(refresh_type_names) / sizeof(refresh_type_names[0]); ++n) { if (s == refresh_type_names[n].name) { refresh_type = refresh_type_names[n].refresh_type; return true; } } fail_msg_writer() << cryptonote::simple_wallet::tr("failed to parse refresh type"); return false; } std::string get_refresh_type_name(tools::wallet2::RefreshType type) { for (size_t n = 0; n < sizeof(refresh_type_names) / sizeof(refresh_type_names[0]); ++n) { if (type == refresh_type_names[n].refresh_type) return refresh_type_names[n].name; } return "invalid"; } std::string get_version_string(uint32_t version) { return boost::lexical_cast(version >> 16) + "." + boost::lexical_cast(version & 0xffff); } std::string oa_prompter(const std::string &url, const std::vector &addresses, bool dnssec_valid) { if (addresses.empty()) return {}; // prompt user for confirmation. // inform user of DNSSEC validation status as well. std::string dnssec_str; if (dnssec_valid) { dnssec_str = tr("DNSSEC validation passed"); } else { dnssec_str = tr("WARNING: DNSSEC validation was unsuccessful, this address may not be correct!"); } std::stringstream prompt; prompt << tr("For URL: ") << url << ", " << dnssec_str << std::endl << tr(" Wownero Address = ") << addresses[0] << std::endl << tr("Is this OK? (Y/n) ") ; // prompt the user for confirmation given the dns query and dnssec status std::string confirm_dns_ok = input_line(prompt.str()); if (std::cin.eof()) { return {}; } if (!command_line::is_yes(confirm_dns_ok)) { std::cout << tr("you have cancelled the transfer request") << std::endl; return {}; } return addresses[0]; } bool parse_subaddress_indices(const std::string& arg, std::set& subaddr_indices) { subaddr_indices.clear(); if (arg.substr(0, 6) != "index=") return false; std::string subaddr_indices_str_unsplit = arg.substr(6, arg.size() - 6); std::vector subaddr_indices_str; boost::split(subaddr_indices_str, subaddr_indices_str_unsplit, boost::is_any_of(",")); for (const auto& subaddr_index_str : subaddr_indices_str) { uint32_t subaddr_index; if(!epee::string_tools::get_xtype_from_string(subaddr_index, subaddr_index_str)) { fail_msg_writer() << tr("failed to parse index: ") << subaddr_index_str; subaddr_indices.clear(); return false; } subaddr_indices.insert(subaddr_index); } return true; } boost::optional> parse_subaddress_lookahead(const std::string& str) { auto r = tools::parse_subaddress_lookahead(str); if (!r) fail_msg_writer() << tr("invalid format for subaddress lookahead; must be :"); return r; } void handle_transfer_exception(const std::exception_ptr &e, bool trusted_daemon) { bool warn_of_possible_attack = !trusted_daemon; try { std::rethrow_exception(e); } catch (const tools::error::daemon_busy&) { fail_msg_writer() << tr("daemon is busy. Please try again later."); } catch (const tools::error::no_connection_to_daemon&) { fail_msg_writer() << tr("no connection to daemon. Please make sure daemon is running."); } catch (const tools::error::wallet_rpc_error& e) { LOG_ERROR("RPC error: " << e.to_string()); fail_msg_writer() << tr("RPC error: ") << e.what(); } catch (const tools::error::get_random_outs_error &e) { fail_msg_writer() << tr("failed to get random outputs to mix: ") << e.what(); } catch (const tools::error::not_enough_unlocked_money& e) { LOG_PRINT_L0(boost::format("not enough money to transfer, available only %s, sent amount %s") % print_money(e.available()) % print_money(e.tx_amount())); fail_msg_writer() << tr("Not enough money in unlocked balance"); warn_of_possible_attack = false; } catch (const tools::error::not_enough_money& e) { LOG_PRINT_L0(boost::format("not enough money to transfer, available only %s, sent amount %s") % print_money(e.available()) % print_money(e.tx_amount())); fail_msg_writer() << tr("Not enough money in unlocked balance"); warn_of_possible_attack = false; } catch (const tools::error::tx_not_possible& e) { LOG_PRINT_L0(boost::format("not enough money to transfer, available only %s, transaction amount %s = %s + %s (fee)") % print_money(e.available()) % print_money(e.tx_amount() + e.fee()) % print_money(e.tx_amount()) % print_money(e.fee())); fail_msg_writer() << tr("Failed to find a way to create transactions. This is usually due to dust which is so small it cannot pay for itself in fees, or trying to send more money than the unlocked balance, or not leaving enough for fees"); warn_of_possible_attack = false; } catch (const tools::error::not_enough_outs_to_mix& e) { auto writer = fail_msg_writer(); writer << tr("not enough outputs for specified ring size") << " = " << (e.mixin_count() + 1) << ":"; for (std::pair outs_for_amount : e.scanty_outs()) { writer << "\n" << tr("output amount") << " = " << print_money(outs_for_amount.first) << ", " << tr("found outputs to use") << " = " << outs_for_amount.second; } writer << tr("Please use sweep_unmixable."); } catch (const tools::error::tx_not_constructed&) { fail_msg_writer() << tr("transaction was not constructed"); warn_of_possible_attack = false; } catch (const tools::error::tx_rejected& e) { fail_msg_writer() << (boost::format(tr("transaction %s was rejected by daemon with status: ")) % get_transaction_hash(e.tx())) << e.status(); std::string reason = e.reason(); if (!reason.empty()) fail_msg_writer() << tr("Reason: ") << reason; } catch (const tools::error::tx_sum_overflow& e) { fail_msg_writer() << e.what(); warn_of_possible_attack = false; } catch (const tools::error::zero_destination&) { fail_msg_writer() << tr("one of destinations is zero"); warn_of_possible_attack = false; } catch (const tools::error::tx_too_big& e) { fail_msg_writer() << tr("failed to find a suitable way to split transactions"); warn_of_possible_attack = false; } catch (const tools::error::transfer_error& e) { LOG_ERROR("unknown transfer error: " << e.to_string()); fail_msg_writer() << tr("unknown transfer error: ") << e.what(); } catch (const tools::error::multisig_export_needed& e) { LOG_ERROR("Multisig error: " << e.to_string()); fail_msg_writer() << tr("Multisig error: ") << e.what(); warn_of_possible_attack = false; } catch (const tools::error::wallet_internal_error& e) { LOG_ERROR("internal error: " << e.to_string()); fail_msg_writer() << tr("internal error: ") << e.what(); } catch (const std::exception& e) { LOG_ERROR("unexpected error: " << e.what()); fail_msg_writer() << tr("unexpected error: ") << e.what(); } if (warn_of_possible_attack) fail_msg_writer() << tr("There was an error, which could mean the node may be trying to get you to retry creating a transaction, and zero in on which outputs you own. Or it could be a bona fide error. It may be prudent to disconnect from this node, and not try to send a tranasction immediately. Alternatively, connect to another node so the original node cannot correlate information."); } bool check_file_overwrite(const std::string &filename) { boost::system::error_code errcode; if (boost::filesystem::exists(filename, errcode)) { if (boost::ends_with(filename, ".keys")) { fail_msg_writer() << boost::format(tr("File %s likely stores wallet private keys! Use a different file name.")) % filename; return false; } return command_line::is_yes(input_line((boost::format(tr("File %s already exists. Are you sure to overwrite it? (Y/Yes/N/No): ")) % filename).str())); } return true; } } bool parse_priority(const std::string& arg, uint32_t& priority) { auto priority_pos = std::find( allowed_priority_strings.begin(), allowed_priority_strings.end(), arg); if(priority_pos != allowed_priority_strings.end()) { priority = std::distance(allowed_priority_strings.begin(), priority_pos); return true; } return false; } std::string join_priority_strings(const char *delimiter) { std::string s; for (size_t n = 0; n < allowed_priority_strings.size(); ++n) { if (!s.empty()) s += delimiter; s += allowed_priority_strings[n]; } return s; } std::string simple_wallet::get_commands_str() { std::stringstream ss; ss << tr("Commands: ") << ENDL; std::string usage = m_cmd_binder.get_usage(); boost::replace_all(usage, "\n", "\n "); usage.insert(0, " "); ss << usage << ENDL; return ss.str(); } std::string simple_wallet::get_command_usage(const std::vector &args) { std::pair documentation = m_cmd_binder.get_documentation(args); std::stringstream ss; if(documentation.first.empty()) { ss << tr("Unknown command: ") << args.front(); } else { std::string usage = documentation.second.empty() ? args.front() : documentation.first; std::string description = documentation.second.empty() ? documentation.first : documentation.second; usage.insert(0, " "); ss << tr("Command usage: ") << ENDL << usage << ENDL << ENDL; boost::replace_all(description, "\n", "\n "); description.insert(0, " "); ss << tr("Command description: ") << ENDL << description << ENDL; } return ss.str(); } bool simple_wallet::viewkey(const std::vector &args/* = std::vector()*/) { if (m_wallet->ask_password() && !get_and_verify_password()) { return true; } // don't log if (m_wallet->key_on_device()) { std::cout << "secret: On device. Not available" << std::endl; } else { std::cout << "secret: " << string_tools::pod_to_hex(m_wallet->get_account().get_keys().m_view_secret_key) << std::endl; } std::cout << "public: " << string_tools::pod_to_hex(m_wallet->get_account().get_keys().m_account_address.m_view_public_key) << std::endl; return true; } bool simple_wallet::spendkey(const std::vector &args/* = std::vector()*/) { if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and has no spend key"); return true; } if (m_wallet->ask_password() && !get_and_verify_password()) { return true; } // don't log if (m_wallet->key_on_device()) { std::cout << "secret: On device. Not available" << std::endl; } else { std::cout << "secret: " << string_tools::pod_to_hex(m_wallet->get_account().get_keys().m_spend_secret_key) << std::endl; } std::cout << "public: " << string_tools::pod_to_hex(m_wallet->get_account().get_keys().m_account_address.m_spend_public_key) << std::endl; return true; } bool simple_wallet::print_seed(bool encrypted) { bool success = false; std::string seed; bool ready, multisig; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and has no seed"); return true; } if (m_wallet->ask_password() && !get_and_verify_password()) { return true; } multisig = m_wallet->multisig(&ready); if (multisig) { if (!ready) { fail_msg_writer() << tr("wallet is multisig but not yet finalized"); return true; } } else if (!m_wallet->is_deterministic()) { fail_msg_writer() << tr("wallet is non-deterministic and has no seed"); return true; } epee::wipeable_string seed_pass; if (encrypted) { auto pwd_container = password_prompter(tr("Enter optional seed encryption passphrase, empty to see raw seed"), true); if (std::cin.eof() || !pwd_container) return true; seed_pass = pwd_container->password(); } if (multisig) success = m_wallet->get_multisig_seed(seed, seed_pass); else if (m_wallet->is_deterministic()) success = m_wallet->get_seed(seed, seed_pass); if (success) { print_seed(seed); } else { fail_msg_writer() << tr("Failed to retrieve seed"); } return true; } bool simple_wallet::seed(const std::vector &args/* = std::vector()*/) { return print_seed(false); } bool simple_wallet::encrypted_seed(const std::vector &args/* = std::vector()*/) { return print_seed(true); } bool simple_wallet::seed_set_language(const std::vector &args/* = std::vector()*/) { if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (m_wallet->multisig()) { fail_msg_writer() << tr("wallet is multisig and has no seed"); return true; } if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and has no seed"); return true; } if (!m_wallet->is_deterministic()) { fail_msg_writer() << tr("wallet is non-deterministic and has no seed"); return true; } const auto pwd_container = get_and_verify_password(); if (pwd_container) { std::string mnemonic_language = get_mnemonic_language(); if (mnemonic_language.empty()) return true; m_wallet->set_seed_language(std::move(mnemonic_language)); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } bool simple_wallet::change_password(const std::vector &args) { const auto orig_pwd_container = get_and_verify_password(); if(orig_pwd_container == boost::none) { fail_msg_writer() << tr("Your original password was incorrect."); return true; } // prompts for a new password, pass true to verify the password const auto pwd_container = default_password_prompter(true); if(!pwd_container) return true; try { m_wallet->rewrite(m_wallet_file, pwd_container->password()); m_wallet->store(); } catch (const tools::error::wallet_logic_error& e) { fail_msg_writer() << tr("Error with wallet rewrite: ") << e.what(); return true; } return true; } bool simple_wallet::print_fee_info(const std::vector &args/* = std::vector()*/) { if (!try_connect_to_daemon()) { fail_msg_writer() << tr("Cannot connect to daemon"); return true; } const uint64_t per_kb_fee = m_wallet->get_per_kb_fee(); const uint64_t typical_size_kb = 13; message_writer() << (boost::format(tr("Current fee is %s wownero per kB")) % print_money(per_kb_fee)).str(); std::vector fees; for (uint32_t priority = 1; priority <= 4; ++priority) { uint64_t mult = m_wallet->get_fee_multiplier(priority); fees.push_back(per_kb_fee * typical_size_kb * mult); } std::vector> blocks; try { uint64_t base_size = typical_size_kb * 1024; blocks = m_wallet->estimate_backlog(base_size, base_size + 1023, fees); } catch (const std::exception &e) { fail_msg_writer() << tr("Error: failed to estimate backlog array size: ") << e.what(); return true; } if (blocks.size() != 4) { fail_msg_writer() << tr("Error: bad estimated backlog array size"); return true; } for (uint32_t priority = 1; priority <= 4; ++priority) { uint64_t nblocks_low = blocks[priority - 1].first; uint64_t nblocks_high = blocks[priority - 1].second; if (nblocks_low > 0) { std::string msg; if (priority == m_wallet->get_default_priority() || (m_wallet->get_default_priority() == 0 && priority == 2)) msg = tr(" (current)"); uint64_t minutes_low = nblocks_low * DIFFICULTY_TARGET_V2 / 60, minutes_high = nblocks_high * DIFFICULTY_TARGET_V2 / 60; if (nblocks_high == nblocks_low) message_writer() << (boost::format(tr("%u block (%u minutes) backlog at priority %u%s")) % nblocks_low % minutes_low % priority % msg).str(); else message_writer() << (boost::format(tr("%u to %u block (%u to %u minutes) backlog at priority %u")) % nblocks_low % nblocks_high % minutes_low % minutes_high % priority).str(); } else message_writer() << tr("No backlog at priority ") << priority; } return true; } bool simple_wallet::prepare_multisig(const std::vector &args) { m_command_successful = false; bool by_mms = called_by_mms(); if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (m_wallet->multisig()) { fail_msg_writer() << tr("This wallet is already multisig"); return true; } if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and cannot be made multisig"); return true; } if(m_wallet->get_num_transfer_details()) { fail_msg_writer() << tr("This wallet has been used before, please use a new wallet to create a multisig wallet"); return true; } const auto orig_pwd_container = get_and_verify_password(); if(orig_pwd_container == boost::none) { fail_msg_writer() << tr("Your password is incorrect."); return true; } std::string multisig_info = m_wallet->get_multisig_info(); success_msg_writer() << multisig_info; success_msg_writer() << tr("Send this multisig info to all other participants, then use make_multisig [...] with others' multisig info"); success_msg_writer() << tr("This includes the PRIVATE view key, so needs to be disclosed only to that multisig wallet's participants "); if (by_mms) { get_message_store().process_wallet_created_data(get_multisig_wallet_state(), mms::message_type::key_set, multisig_info); } m_command_successful = true; return true; } bool simple_wallet::make_multisig(const std::vector &args) { m_command_successful = false; bool by_mms = called_by_mms(); if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (m_wallet->multisig()) { fail_msg_writer() << tr("This wallet is already multisig"); return true; } if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and cannot be made multisig"); return true; } if(m_wallet->get_num_transfer_details()) { fail_msg_writer() << tr("This wallet has been used before, please use a new wallet to create a multisig wallet"); return true; } const auto orig_pwd_container = get_and_verify_password(); if(orig_pwd_container == boost::none) { fail_msg_writer() << tr("Your original password was incorrect."); return true; } if (args.size() < 2) { fail_msg_writer() << tr("usage: make_multisig [...]"); return true; } // parse threshold uint32_t threshold; if (!string_tools::get_xtype_from_string(threshold, args[0])) { fail_msg_writer() << tr("Invalid threshold"); return true; } LOCK_IDLE_SCOPE(); try { auto local_args = args; local_args.erase(local_args.begin()); std::string multisig_extra_info = m_wallet->make_multisig(orig_pwd_container->password(), local_args, threshold); if (!multisig_extra_info.empty()) { success_msg_writer() << tr("Another step is needed"); success_msg_writer() << multisig_extra_info; success_msg_writer() << tr("Send this multisig info to all other participants, then use finalize_multisig [...] with others' multisig info"); if (by_mms) { get_message_store().process_wallet_created_data(get_multisig_wallet_state(), mms::message_type::finalizing_key_set, multisig_extra_info); } m_command_successful = true; return true; } } catch (const std::exception &e) { fail_msg_writer() << tr("Error creating multisig: ") << e.what(); return true; } uint32_t total; if (!m_wallet->multisig(NULL, &threshold, &total)) { fail_msg_writer() << tr("Error creating multisig: new wallet is not multisig"); return true; } success_msg_writer() << std::to_string(threshold) << "/" << total << tr(" multisig address: ") << m_wallet->get_account().get_public_address_str(m_wallet->nettype()); m_command_successful = true; return true; } bool simple_wallet::finalize_multisig(const std::vector &args) { m_command_successful = false; bool by_mms = called_by_mms(); bool ready; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (!m_wallet->multisig(&ready)) { fail_msg_writer() << tr("This wallet is not multisig"); return true; } if (ready) { fail_msg_writer() << tr("This wallet is already finalized"); return true; } const auto orig_pwd_container = get_and_verify_password(); if(orig_pwd_container == boost::none) { fail_msg_writer() << tr("Your original password was incorrect."); return true; } if (args.size() < 2) { fail_msg_writer() << tr("usage: finalize_multisig [...]"); return true; } try { if (!m_wallet->finalize_multisig(orig_pwd_container->password(), args)) { fail_msg_writer() << tr("Failed to finalize multisig"); return true; } } catch (const std::exception &e) { fail_msg_writer() << tr("Failed to finalize multisig: ") << e.what(); return true; } m_command_successful = true; return true; } bool simple_wallet::export_multisig(const std::vector &args) { m_command_successful = false; bool by_mms = called_by_mms(); bool ready; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (!m_wallet->multisig(&ready)) { fail_msg_writer() << tr("This wallet is not multisig"); return true; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return true; } if (args.size() != 1) { fail_msg_writer() << tr("usage: export_multisig_info "); return true; } if (m_wallet->ask_password() && !get_and_verify_password()) return true; const std::string filename = args[0]; if (!by_mms && m_wallet->confirm_export_overwrite() && !check_file_overwrite(filename)) return true; try { cryptonote::blobdata ciphertext = m_wallet->export_multisig(); if (by_mms) { get_message_store().process_wallet_created_data(get_multisig_wallet_state(), mms::message_type::multisig_sync_data, ciphertext); } else { bool r = epee::file_io_utils::save_string_to_file(filename, ciphertext); if (!r) { fail_msg_writer() << tr("failed to save file ") << filename; return true; } } } catch (const std::exception &e) { LOG_ERROR("Error exporting multisig info: " << e.what()); fail_msg_writer() << tr("Error exporting multisig info: ") << e.what(); return true; } success_msg_writer() << tr("Multisig info exported to ") << filename; m_command_successful = true; return true; } bool simple_wallet::import_multisig(const std::vector &args) { m_command_successful = false; bool by_mms = called_by_mms(); bool ready; uint32_t threshold, total; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (!m_wallet->multisig(&ready, &threshold, &total)) { fail_msg_writer() << tr("This wallet is not multisig"); return true; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return true; } if (args.size() < threshold - 1) { fail_msg_writer() << tr("usage: import_multisig_info [...] - one for each other participant"); return true; } if (m_wallet->ask_password() && !get_and_verify_password()) return true; std::vector info; for (size_t n = 0; n < args.size(); ++n) { if (by_mms) { info.push_back(args[n]); } else { const std::string &filename = args[n]; std::string data; bool r = epee::file_io_utils::load_file_to_string(filename, data); if (!r) { fail_msg_writer() << tr("failed to read file ") << filename; return true; } info.push_back(std::move(data)); } } LOCK_IDLE_SCOPE(); // all read and parsed, actually import try { size_t n_outputs = m_wallet->import_multisig(info); // Clear line "Height xxx of xxx" std::cout << "\r \r"; success_msg_writer() << tr("Multisig info imported"); m_command_successful = true; } catch (const std::exception &e) { fail_msg_writer() << tr("Failed to import multisig info: ") << e.what(); return true; } if (is_daemon_trusted()) { try { m_wallet->rescan_spent(); } catch (const std::exception &e) { message_writer() << tr("Failed to update spent status after importing multisig info: ") << e.what(); } } else { message_writer() << tr("Untrusted daemon, spent status may be incorrect. Use a trusted daemon and run \"rescan_spent\""); } return true; } bool simple_wallet::accept_loaded_tx(const tools::wallet2::multisig_tx_set &txs) { std::string extra_message; return accept_loaded_tx([&txs](){return txs.m_ptx.size();}, [&txs](size_t n)->const tools::wallet2::tx_construction_data&{return txs.m_ptx[n].construction_data;}, extra_message); } bool simple_wallet::sign_multisig(const std::vector &args) { m_command_successful = false; bool by_mms = called_by_mms(); bool ready; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if(!m_wallet->multisig(&ready)) { fail_msg_writer() << tr("This is not a multisig wallet"); return true; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return true; } if (args.size() != 1) { fail_msg_writer() << tr("usage: sign_multisig "); return true; } if (m_wallet->ask_password() && !get_and_verify_password()) { return true; } std::string filename = args[0]; std::vector txids; uint32_t signers = 0; try { if (by_mms) { tools::wallet2::multisig_tx_set exported_txs; std::string ciphertext; bool r = m_wallet->load_multisig_tx(args[0], exported_txs, [&](const tools::wallet2::multisig_tx_set &tx){ signers = tx.m_signers.size(); return accept_loaded_tx(tx); }); if (r) { r = m_wallet->sign_multisig_tx(exported_txs, txids); } if (r) { ciphertext = m_wallet->save_multisig_tx(exported_txs); if (ciphertext.empty()) { r = false; } } if (r) { mms::message_type message_type = mms::message_type::fully_signed_tx; if (txids.empty()) { message_type = mms::message_type::partially_signed_tx; } get_message_store().process_wallet_created_data(get_multisig_wallet_state(), message_type, ciphertext); filename = "MMS"; // for the messages below m_command_successful = true; } else { fail_msg_writer() << tr("Failed to sign multisig transaction"); return true; } } else { bool r = m_wallet->sign_multisig_tx_from_file(filename, txids, [&](const tools::wallet2::multisig_tx_set &tx){ signers = tx.m_signers.size(); return accept_loaded_tx(tx); }); if (!r) { fail_msg_writer() << tr("Failed to sign multisig transaction"); return true; } } } catch (const tools::error::multisig_export_needed& e) { fail_msg_writer() << tr("Multisig error: ") << e.what(); return true; } catch (const std::exception &e) { fail_msg_writer() << tr("Failed to sign multisig transaction: ") << e.what(); return true; } if (txids.empty()) { uint32_t threshold; m_wallet->multisig(NULL, &threshold); uint32_t signers_needed = threshold - signers - 1; success_msg_writer(true) << tr("Transaction successfully signed to file ") << filename << ", " << signers_needed << " more signer(s) needed"; return true; } else { std::string txids_as_text; for (const auto &txid: txids) { if (!txids_as_text.empty()) txids_as_text += (", "); txids_as_text += epee::string_tools::pod_to_hex(txid); } success_msg_writer(true) << tr("Transaction successfully signed to file ") << filename << ", txid " << txids_as_text; success_msg_writer(true) << tr("It may be relayed to the network with submit_multisig"); } return true; } bool simple_wallet::submit_multisig(const std::vector &args) { m_command_successful = false; bool by_mms = called_by_mms(); bool ready; uint32_t threshold; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (!m_wallet->multisig(&ready, &threshold)) { fail_msg_writer() << tr("This is not a multisig wallet"); return true; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return true; } if (args.size() != 1) { fail_msg_writer() << tr("usage: submit_multisig "); return true; } if (m_wallet->ask_password() && !get_and_verify_password()) { return true; } if (!try_connect_to_daemon()) return true; std::string filename = args[0]; try { tools::wallet2::multisig_tx_set txs; if (by_mms) { bool r = m_wallet->load_multisig_tx(args[0], txs, [&](const tools::wallet2::multisig_tx_set &tx){ return accept_loaded_tx(tx); }); if (!r) { fail_msg_writer() << tr("Failed to load multisig transaction from MMS"); return true; } } else { bool r = m_wallet->load_multisig_tx_from_file(filename, txs, [&](const tools::wallet2::multisig_tx_set &tx){ return accept_loaded_tx(tx); }); if (!r) { fail_msg_writer() << tr("Failed to load multisig transaction from file"); return true; } } if (txs.m_signers.size() < threshold) { fail_msg_writer() << (boost::format(tr("Multisig transaction signed by only %u signers, needs %u more signatures")) % txs.m_signers.size() % (threshold - txs.m_signers.size())).str(); return true; } // actually commit the transactions for (auto &ptx: txs.m_ptx) { m_wallet->commit_tx(ptx); success_msg_writer(true) << tr("Transaction successfully submitted, transaction ") << get_transaction_hash(ptx.tx) << ENDL << tr("You can check its status by using the `show_transfers` command."); } m_command_successful = true; } catch (const std::exception &e) { handle_transfer_exception(std::current_exception(), is_daemon_trusted()); } catch (...) { LOG_ERROR("unknown error"); fail_msg_writer() << tr("unknown error"); } return true; } bool simple_wallet::export_raw_multisig(const std::vector &args) { bool ready; uint32_t threshold; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } if (!m_wallet->multisig(&ready, &threshold)) { fail_msg_writer() << tr("This is not a multisig wallet"); return true; } if (!ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return true; } if (args.size() != 1) { fail_msg_writer() << tr("usage: export_raw_multisig "); return true; } if (m_wallet->ask_password() && !get_and_verify_password()) { return true; } std::string filename = args[0]; if (m_wallet->confirm_export_overwrite() && !check_file_overwrite(filename)) return true; try { tools::wallet2::multisig_tx_set txs; bool r = m_wallet->load_multisig_tx_from_file(filename, txs, [&](const tools::wallet2::multisig_tx_set &tx){ return accept_loaded_tx(tx); }); if (!r) { fail_msg_writer() << tr("Failed to load multisig transaction from file"); return true; } if (txs.m_signers.size() < threshold) { fail_msg_writer() << (boost::format(tr("Multisig transaction signed by only %u signers, needs %u more signatures")) % txs.m_signers.size() % (threshold - txs.m_signers.size())).str(); return true; } // save the transactions std::string filenames; for (auto &ptx: txs.m_ptx) { const crypto::hash txid = cryptonote::get_transaction_hash(ptx.tx); const std::string filename = std::string("raw_multisig_wownero_tx_") + epee::string_tools::pod_to_hex(txid); if (!filenames.empty()) filenames += ", "; filenames += filename; if (!epee::file_io_utils::save_string_to_file(filename, cryptonote::tx_to_blob(ptx.tx))) { fail_msg_writer() << tr("Failed to export multisig transaction to file ") << filename; return true; } } success_msg_writer() << tr("Saved exported multisig transaction file(s): ") << filenames; } catch (const std::exception& e) { LOG_ERROR("unexpected error: " << e.what()); fail_msg_writer() << tr("unexpected error: ") << e.what(); } catch (...) { LOG_ERROR("Unknown error"); fail_msg_writer() << tr("unknown error"); } return true; } // MMS --------------------------------------------------------------------------------------------------- // Access to the message store, or more exactly to the list of the messages that can be changed // by the idle thread, is guarded by the same mutex-based mechanism as access to the wallet // as a whole and thus e.g. uses the "LOCK_IDLE_SCOPE" macro. This is a little over-cautious, but // simple and safe. Care has to be taken however where MMS methods call other simplewallet methods // that use "LOCK_IDLE_SCOPE" as this cannot be nested! // Methods for commands like "export_multisig_info" usually read data from file(s) or write data // to files. The MMS calls now those methods as well, to produce data for messages and to process data // from messages. As it would be quite inconvenient for the MMS to write data for such methods to files // first or get data out of result files after the call, those methods detect a call from the MMS and // expect data as arguments instead of files and give back data by calling 'process_wallet_created_data'. bool simple_wallet::called_by_mms() { bool by_mms = m_called_by_mms; m_called_by_mms = false; return by_mms; } bool simple_wallet::user_confirms(const std::string &question) { std::string answer = input_line(question + tr(" (Y/Yes/N/No): ")); return !std::cin.eof() && command_line::is_yes(answer); } bool simple_wallet::get_number_from_arg(const std::string &arg, uint32_t &number, const uint32_t lower_bound, const uint32_t upper_bound) { bool valid = false; try { number = boost::lexical_cast(arg); valid = (number >= lower_bound) && (number <= upper_bound); } catch(const boost::bad_lexical_cast &) { } return valid; } bool simple_wallet::choose_mms_processing(const std::vector &data_list, uint32_t &choice) { uint32_t choices = data_list.size(); if (choices == 1) { choice = 0; return true; } mms::message_store& ms = m_wallet->get_message_store(); success_msg_writer() << tr("Choose processing:"); std::string text; for (size_t i = 0; i < choices; ++i) { const mms::processing_data &data = data_list[i]; text = std::to_string(i+1) + ": "; switch (data.processing) { case mms::message_processing::sign_tx: text += tr("Sign tx"); break; case mms::message_processing::send_tx: { mms::message m; ms.get_message_by_id(data.message_ids[0], m); if (m.type == mms::message_type::fully_signed_tx) { text += tr("Send the tx for submission to "); } else { text += tr("Send the tx for signing to "); } mms::coalition_member member = ms.get_member(data.receiving_member_index); text += ms.member_to_string(member, 50); break; } case mms::message_processing::submit_tx: text += tr("Submit tx"); break; default: text += tr("unknown"); break; } success_msg_writer() << text; } std::string line = input_line(tr("Choice: ")); if (std::cin.eof() || line.empty()) { return false; } bool choice_ok = get_number_from_arg(line, choice, 1, choices); if (choice_ok) { choice--; } else { fail_msg_writer() << tr("Wrong choice"); } return choice_ok; } static std::string get_human_readable_timestamp(uint64_t ts); static std::string get_human_readable_timespan(std::chrono::seconds seconds); void simple_wallet::list_mms_messages(const std::vector &messages) { success_msg_writer() << boost::format("%4s %-4s %-30s %-21s %7s %-15s %-40s") % tr("Id") % tr("I/O") % tr("Coalition Member") % tr("Message Type") % tr("Height") % tr("Message State") % tr("Since"); mms::message_store& ms = m_wallet->get_message_store(); uint64_t now = time(NULL); for (size_t i = 0; i < messages.size(); ++i) { const mms::message &m = messages[i]; const mms::coalition_member &member = ms.get_member(m.member_index); bool highlight = (m.state == mms::message_state::ready_to_send) || (m.state == mms::message_state::waiting); message_writer(m.direction == mms::message_direction::out ? console_color_green : console_color_magenta, highlight) << boost::format("%4s %-4s %-30s %-21s %7s %-15s %-40s") % m.id % ms.message_direction_to_string(m.direction) % ms.member_to_string(member, 30) % ms.message_type_to_string(m.type) % m.wallet_height % ms.message_state_to_string(m.state) % (get_human_readable_timestamp(m.modified) + ", " + get_human_readable_timespan(std::chrono::seconds(now - m.modified)) + tr(" ago")); } } void simple_wallet::show_message(const mms::message &m) { mms::message_store& ms = m_wallet->get_message_store(); const mms::coalition_member &member = ms.get_member(m.member_index); bool display_content; switch (m.type) { case mms::message_type::key_set: case mms::message_type::finalizing_key_set: case mms::message_type::note: display_content = true; break; default: display_content = false; } uint64_t now = time(NULL); success_msg_writer() << ""; success_msg_writer() << tr("Message ") << m.id; success_msg_writer() << tr("In/out: ") << ms.message_direction_to_string(m.direction); success_msg_writer() << tr("Type: ") << ms.message_type_to_string(m.type); success_msg_writer() << tr("State: ") << boost::format(tr("%s since %s, %s ago")) % ms.message_state_to_string(m.state) % get_human_readable_timestamp(m.modified) % get_human_readable_timespan(std::chrono::seconds(now - m.modified)); if (m.sent == 0) { success_msg_writer() << tr("Sent: Never"); } else { success_msg_writer() << boost::format(tr("Sent: %s, %s ago")) % get_human_readable_timestamp(m.sent) % get_human_readable_timespan(std::chrono::seconds(now - m.sent)); } success_msg_writer() << tr("Member: ") << ms.member_to_string(member, 100); success_msg_writer() << tr("Content size: ") << m.content.length() << tr(" bytes"); success_msg_writer() << tr("Content: ") << (display_content ? m.content : tr("(binary data)")); if (m.type == mms::message_type::note) { // Showing a note and read its text is "processing" it: Set the state accordingly // which will also delete it from Bitmessage as a side effect // (Without this little "twist" it would never change the state, and never get deleted) ms.set_message_processed_or_sent(m.id); } } void simple_wallet::ask_send_all_ready_messages() { mms::message_store& ms = m_wallet->get_message_store(); std::vector ready_messages; const std::vector &messages = ms.get_all_messages(); for (size_t i = 0; i < messages.size(); ++i) { const mms::message &m = messages[i]; if (m.state == mms::message_state::ready_to_send) { ready_messages.push_back(m); } } if (ready_messages.size() != 0) { list_mms_messages(ready_messages); bool send = ms.get_auto_send(); if (!send) { send = user_confirms(tr("Send these messages now?")); } if (send) { mms::multisig_wallet_state state = get_multisig_wallet_state(); for (size_t i = 0; i < ready_messages.size(); ++i) { ms.send_message(state, ready_messages[i].id); ms.set_message_processed_or_sent(ready_messages[i].id); } success_msg_writer() << tr("Sent."); } } } bool simple_wallet::get_message_from_arg(const std::string &arg, mms::message &m) { mms::message_store& ms = m_wallet->get_message_store(); bool valid_id = false; uint32_t id; try { id = (uint32_t)boost::lexical_cast(arg); valid_id = ms.get_message_by_id(id, m); } catch (const boost::bad_lexical_cast &) { } if (!valid_id) { fail_msg_writer() << tr("Invalid message id"); } return valid_id; } void simple_wallet::mms_init(const std::vector &args) { // mms init / // Example: mms init 2/3 rbrunner BM-2cUVEbbb3H6ojddYQziK3RafJ5GPcFQv7e // For now, assume we still have the original Monero address available, before "make_multisig" if (args.size() != 4) { fail_msg_writer() << tr("usage: mms init / "); return; } mms::message_store& ms = m_wallet->get_message_store(); if (ms.get_active()) { if (!user_confirms(tr("The MMS is already initialized. Re-initialize by deleting all member info and messages?"))) { return; } } uint32_t threshold; uint32_t coalition_size; const std::string &mn = args[1]; std::vector numbers; boost::split(numbers, mn, boost::is_any_of("/")); bool mn_ok = (numbers.size() == 2) && get_number_from_arg(numbers[0], threshold, 1, 100) && get_number_from_arg(numbers[1], coalition_size, 2, 100); if (mn_ok) { mn_ok = (threshold == coalition_size) || (threshold == (coalition_size -1)); // Fully general cases like 3/5 not yet supported } if (!mn_ok) { fail_msg_writer() << tr("Error in threshold and/or coalition size"); return; } ms.init(get_multisig_wallet_state(), args[2], args[3], coalition_size, threshold); } void simple_wallet::mms_info(const std::vector &args) { mms::message_store& ms = m_wallet->get_message_store(); success_msg_writer() << boost::format("The MMS is active for %s/%s multisig.") % ms.get_threshold() % ms.get_coalition_size(); } void simple_wallet::mms_member(const std::vector &args) // mms 0:member [1: <2:label> [3: [4:]]] { mms::message_store& ms = m_wallet->get_message_store(); const std::vector &members = ms.get_all_members(); if (args.size() == 1) { // Without further parameters list all defined members success_msg_writer() << boost::format("%2s %-20s %-s") % tr("#") % tr("Label") % tr("Transport Address"); success_msg_writer() << boost::format("%2s %-20s %-s") % "" % "" % tr("Wownero Address"); for (size_t i = 0; i < members.size(); ++i) { const mms::coalition_member &member = members[i]; std::string label = member.label.empty() ? tr("") : member.label; std::string monero_address; if (member.monero_address_known) { monero_address = get_account_address_as_str(m_wallet->nettype(), false, member.monero_address); } else { monero_address = tr(""); } std::string transport_address = member.transport_address.empty() ? tr("") : member.transport_address; success_msg_writer() << boost::format("%2s %-20s %-s") % (i + 1) % label % transport_address; success_msg_writer() << boost::format("%2s %-20s %-s") % "" % "" % monero_address; success_msg_writer() << ""; } return; } uint32_t index; bool index_valid = get_number_from_arg(args[1], index, 1, ms.get_coalition_size()); if (index_valid) { index--; } else { fail_msg_writer() << tr("Invalid coalition member number ") + args[1]; return; } if (args.size() < 3) { fail_msg_writer() << tr("mms member [