// Copyright (c) 2014-2017, 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 "wallet/wallet_args.h" #include #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 DEFAULT_MIX 4 #define MIN_RING_SIZE 5 // Used to inform user about min ring size -- does not track actual protocol #define OUTPUT_EXPORT_FILE_MAGIC "Monero output export\003" #define MULTISIG_EXPORT_FILE_MAGIC "Monero multisig export\001" #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(); \ m_idle_mutex.lock(); \ while (m_auto_refresh_refreshing) \ m_idle_cond.notify_one(); \ m_idle_mutex.unlock(); \ /* if (auto_refresh_run)*/ \ /*m_auto_refresh_thread.join();*/ \ boost::unique_lock lock(m_idle_mutex); \ 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_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_non_deterministic = {"non-deterministic", sw::tr("Create 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_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 monero network"), false}; const command_line::arg_descriptor< std::vector > arg_command = {"command", ""}; 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); 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 new wallet password") : 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(" Monero 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; } void handle_transfer_exception(const std::exception_ptr &e) { 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"); } 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"); } 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"); } 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; } } catch (const tools::error::tx_not_constructed&) { fail_msg_writer() << tr("transaction was not constructed"); } 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(); } catch (const tools::error::zero_destination&) { fail_msg_writer() << tr("one of destinations is zero"); } catch (const tools::error::tx_too_big& e) { fail_msg_writer() << tr("failed to find a suitable way to split transactions"); } 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(); } 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(); } } } 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 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 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 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 electrum_words; 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->ask_password() && !get_and_verify_password()) { return true; } if (m_wallet->is_deterministic()) { if (m_wallet->get_seed_language().empty()) { std::string mnemonic_language = get_mnemonic_language(); if (mnemonic_language.empty()) return true; m_wallet->set_seed_language(mnemonic_language); } epee::wipeable_string seed_pass; if (encrypted) { auto pwd_container = tools::password_container::prompt(true, tr("Enter optional seed encryption passphrase, empty to see raw seed")); if (std::cin.eof() || !pwd_container) return true; seed_pass = pwd_container->password(); } success = m_wallet->get_seed(electrum_words, seed_pass); } if (success) { print_seed(electrum_words); } else { fail_msg_writer() << tr("wallet is non-deterministic and has no 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->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); 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::payment_id(const std::vector &args/* = std::vector()*/) { crypto::hash payment_id; if (args.size() > 0) { fail_msg_writer() << tr("usage: payment_id"); return true; } payment_id = crypto::rand(); success_msg_writer() << tr("Random payment ID: ") << payment_id; 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 monero 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) { 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 "); return true; } bool simple_wallet::make_multisig(const std::vector &args) { 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; } // parse all multisig info std::vector secret_keys(args.size() - 1); std::vector public_keys(args.size() - 1); for (size_t i = 1; i < args.size(); ++i) { if (!tools::wallet2::verify_multisig_info(args[i], secret_keys[i - 1], public_keys[i - 1])) { fail_msg_writer() << tr("Bad multisig info: ") << args[i]; return true; } } // remove duplicates for (size_t i = 0; i < secret_keys.size(); ++i) { for (size_t j = i + 1; j < secret_keys.size(); ++j) { if (rct::sk2rct(secret_keys[i]) == rct::sk2rct(secret_keys[j])) { message_writer() << tr("Duplicate key found, ignoring"); secret_keys[j] = secret_keys.back(); public_keys[j] = public_keys.back(); secret_keys.pop_back(); public_keys.pop_back(); --j; } } } // people may include their own, weed it out const crypto::secret_key local_skey = m_wallet->get_account().get_keys().m_view_secret_key; const crypto::public_key local_pkey = m_wallet->get_account().get_keys().m_account_address.m_spend_public_key; for (size_t i = 0; i < secret_keys.size(); ++i) { if (secret_keys[i] == local_skey) { message_writer() << tr("Local key is present, ignoring"); secret_keys[i] = secret_keys.back(); public_keys[i] = public_keys.back(); secret_keys.pop_back(); public_keys.pop_back(); --i; } else if (public_keys[i] == local_pkey) { fail_msg_writer() << tr("Found local spend public key, but not local view secret key - something very weird"); return true; } } LOCK_IDLE_SCOPE(); try { std::string multisig_extra_info = m_wallet->make_multisig(orig_pwd_container->password(), secret_keys, public_keys, 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"); return true; } } catch (const std::exception &e) { fail_msg_writer() << tr("Error creating multisig: ") << e.what(); return true; } uint32_t total = secret_keys.size() + 1; success_msg_writer() << std::to_string(threshold) << "/" << total << tr(" multisig address: ") << m_wallet->get_account().get_public_address_str(m_wallet->testnet()); return true; } bool simple_wallet::finalize_multisig(const std::vector &args) { bool ready; 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; } return true; } bool simple_wallet::export_multisig(const std::vector &args) { bool ready; 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]; try { std::vector outs = m_wallet->export_multisig(); std::stringstream oss; boost::archive::portable_binary_oarchive ar(oss); ar << outs; std::string magic(MULTISIG_EXPORT_FILE_MAGIC, strlen(MULTISIG_EXPORT_FILE_MAGIC)); const cryptonote::account_public_address &keys = m_wallet->get_account().get_keys().m_account_address; std::string header; header += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key)); header += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key)); crypto::public_key signer = m_wallet->get_multisig_signer_public_key(); header += std::string((const char *)&signer, sizeof(crypto::public_key)); std::string ciphertext = m_wallet->encrypt_with_view_secret_key(header + oss.str()); bool r = epee::file_io_utils::save_string_to_file(filename, magic + 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; return true; } bool simple_wallet::import_multisig(const std::vector &args) { bool ready; uint32_t threshold, total; 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; std::unordered_set seen; for (size_t n = 0; n < args.size(); ++n) { 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; } const size_t magiclen = strlen(MULTISIG_EXPORT_FILE_MAGIC); if (data.size() < magiclen || memcmp(data.data(), MULTISIG_EXPORT_FILE_MAGIC, magiclen)) { fail_msg_writer() << tr("Bad multisig info file magic in ") << filename; return true; } try { data = m_wallet->decrypt_with_view_secret_key(std::string(data, magiclen)); } catch (const std::exception &e) { fail_msg_writer() << tr("Failed to decrypt ") << filename << ": " << e.what(); return true; } const size_t headerlen = 3 * sizeof(crypto::public_key); if (data.size() < headerlen) { fail_msg_writer() << tr("Bad data size from file ") << filename; return true; } const crypto::public_key &public_spend_key = *(const crypto::public_key*)&data[0]; const crypto::public_key &public_view_key = *(const crypto::public_key*)&data[sizeof(crypto::public_key)]; const crypto::public_key &signer = *(const crypto::public_key*)&data[2*sizeof(crypto::public_key)]; const cryptonote::account_public_address &keys = m_wallet->get_account().get_keys().m_account_address; if (public_spend_key != keys.m_spend_public_key || public_view_key != keys.m_view_public_key) { fail_msg_writer() << (boost::format(tr("Multisig info from %s is for a different account")) % filename).str(); return true; } if (m_wallet->get_multisig_signer_public_key() == signer) { message_writer() << (boost::format(tr("Multisig info from %s is from this wallet, ignored")) % filename).str(); continue; } if (seen.find(signer) != seen.end()) { message_writer() << (boost::format(tr("Multisig info from %s already seen, ignored")) % filename).str(); continue; } seen.insert(signer); try { std::string body(data, headerlen); std::istringstream iss(body); std::vector i; boost::archive::portable_binary_iarchive ar(iss); ar >> i; message_writer() << (boost::format(tr("%u outputs found in %s")) % boost::lexical_cast(i.size()) % filename).str(); info.push_back(std::move(i)); } catch (const std::exception &e) { fail_msg_writer() << tr("Failed to import multisig info: ") << e.what(); return true; } } 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"); } catch (const std::exception &e) { fail_msg_writer() << tr("Failed to import multisig info: ") << e.what(); return true; } if (m_trusted_daemon) { 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) { bool ready; 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 { 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) { bool ready; uint32_t threshold; 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; 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."); } } catch (const std::exception &e) { handle_transfer_exception(std::current_exception()); } catch (...) { LOG_ERROR("unknown error"); fail_msg_writer() << tr("unknown error"); } return true; } bool simple_wallet::set_always_confirm_transfers(const std::vector &args/* = std::vector()*/) { const auto pwd_container = get_and_verify_password(); if (pwd_container) { parse_bool_and_use(args[1], [&](bool r) { m_wallet->always_confirm_transfers(r); m_wallet->rewrite(m_wallet_file, pwd_container->password()); }); } return true; } bool simple_wallet::set_print_ring_members(const std::vector &args/* = std::vector()*/) { const auto pwd_container = get_and_verify_password(); if (pwd_container) { parse_bool_and_use(args[1], [&](bool r) { m_wallet->print_ring_members(r); m_wallet->rewrite(m_wallet_file, pwd_container->password()); }); } return true; } bool simple_wallet::set_store_tx_info(const std::vector &args/* = std::vector()*/) { if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and cannot transfer"); return true; } const auto pwd_container = get_and_verify_password(); if (pwd_container) { parse_bool_and_use(args[1], [&](bool r) { m_wallet->store_tx_info(r); m_wallet->rewrite(m_wallet_file, pwd_container->password()); }); } return true; } bool simple_wallet::set_default_ring_size(const std::vector &args/* = std::vector()*/) { if (m_wallet->watch_only()) { fail_msg_writer() << tr("wallet is watch-only and cannot transfer"); return true; } try { if (strchr(args[1].c_str(), '-')) { fail_msg_writer() << tr("ring size must be an integer >= ") << MIN_RING_SIZE; return true; } uint32_t ring_size = boost::lexical_cast(args[1]); if (ring_size < MIN_RING_SIZE && ring_size != 0) { fail_msg_writer() << tr("ring size must be an integer >= ") << MIN_RING_SIZE; return true; } const auto pwd_container = get_and_verify_password(); if (pwd_container) { m_wallet->default_mixin(ring_size > 0 ? ring_size - 1 : 0); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } catch(const boost::bad_lexical_cast &) { fail_msg_writer() << tr("ring size must be an integer >= ") << MIN_RING_SIZE; return true; } catch(...) { fail_msg_writer() << tr("could not change default ring size"); return true; } } bool simple_wallet::set_default_priority(const std::vector &args/* = std::vector()*/) { int priority = 0; try { if (strchr(args[1].c_str(), '-')) { fail_msg_writer() << tr("priority must be 0, 1, 2, 3, or 4 "); return true; } if (args[1] == "0") { priority = 0; } else { priority = boost::lexical_cast(args[1]); if (priority < 1 || priority > 4) { fail_msg_writer() << tr("priority must be 0, 1, 2, 3,or 4"); return true; } } const auto pwd_container = get_and_verify_password(); if (pwd_container) { m_wallet->set_default_priority(priority); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } catch(const boost::bad_lexical_cast &) { fail_msg_writer() << tr("priority must be 0, 1, 2 3,or 4"); return true; } catch(...) { fail_msg_writer() << tr("could not change default priority"); return true; } } bool simple_wallet::set_auto_refresh(const std::vector &args/* = std::vector()*/) { const auto pwd_container = get_and_verify_password(); if (pwd_container) { parse_bool_and_use(args[1], [&](bool auto_refresh) { m_wallet->auto_refresh(auto_refresh); m_idle_mutex.lock(); m_auto_refresh_enabled.store(auto_refresh, std::memory_order_relaxed); m_idle_cond.notify_one(); m_idle_mutex.unlock(); m_wallet->rewrite(m_wallet_file, pwd_container->password()); }); } return true; } bool simple_wallet::set_refresh_type(const std::vector &args/* = std::vector()*/) { tools::wallet2::RefreshType refresh_type; if (!parse_refresh_type(args[1], refresh_type)) { return true; } const auto pwd_container = get_and_verify_password(); if (pwd_container) { m_wallet->set_refresh_type(refresh_type); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } bool simple_wallet::set_confirm_missing_payment_id(const std::vector &args/* = std::vector()*/) { const auto pwd_container = get_and_verify_password(); if (pwd_container) { parse_bool_and_use(args[1], [&](bool r) { m_wallet->confirm_missing_payment_id(r); m_wallet->rewrite(m_wallet_file, pwd_container->password()); }); } return true; } bool simple_wallet::set_ask_password(const std::vector &args/* = std::vector()*/) { const auto pwd_container = get_and_verify_password(); if (pwd_container) { parse_bool_and_use(args[1], [&](bool r) { m_wallet->ask_password(r); m_wallet->rewrite(m_wallet_file, pwd_container->password()); }); } return true; } bool simple_wallet::set_unit(const std::vector &args/* = std::vector()*/) { const std::string &unit = args[1]; unsigned int decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT; if (unit == "monero") decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT; else if (unit == "millinero") decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT - 3; else if (unit == "micronero") decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT - 6; else if (unit == "nanonero") decimal_point = CRYPTONOTE_DISPLAY_DECIMAL_POINT - 9; else if (unit == "piconero") decimal_point = 0; else { fail_msg_writer() << tr("invalid unit"); return true; } const auto pwd_container = get_and_verify_password(); if (pwd_container) { m_wallet->set_default_decimal_point(decimal_point); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } bool simple_wallet::set_min_output_count(const std::vector &args/* = std::vector()*/) { uint32_t count; if (!string_tools::get_xtype_from_string(count, args[1])) { fail_msg_writer() << tr("invalid count: must be an unsigned integer"); return true; } const auto pwd_container = get_and_verify_password(); if (pwd_container) { m_wallet->set_min_output_count(count); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } bool simple_wallet::set_min_output_value(const std::vector &args/* = std::vector()*/) { uint64_t value; if (!cryptonote::parse_amount(value, args[1])) { fail_msg_writer() << tr("invalid value"); return true; } const auto pwd_container = get_and_verify_password(); if (pwd_container) { m_wallet->set_min_output_value(value); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } bool simple_wallet::set_merge_destinations(const std::vector &args/* = std::vector()*/) { const auto pwd_container = get_and_verify_password(); if (pwd_container) { parse_bool_and_use(args[1], [&](bool r) { m_wallet->merge_destinations(r); m_wallet->rewrite(m_wallet_file, pwd_container->password()); }); } return true; } bool simple_wallet::set_confirm_backlog(const std::vector &args/* = std::vector()*/) { const auto pwd_container = get_and_verify_password(); if (pwd_container) { parse_bool_and_use(args[1], [&](bool r) { m_wallet->confirm_backlog(r); m_wallet->rewrite(m_wallet_file, pwd_container->password()); }); } return true; } bool simple_wallet::set_confirm_backlog_threshold(const std::vector &args/* = std::vector()*/) { uint32_t threshold; if (!string_tools::get_xtype_from_string(threshold, args[1])) { fail_msg_writer() << tr("invalid count: must be an unsigned integer"); return true; } const auto pwd_container = get_and_verify_password(); if (pwd_container) { m_wallet->set_confirm_backlog_threshold(threshold); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } bool simple_wallet::set_refresh_from_block_height(const std::vector &args/* = std::vector()*/) { const auto pwd_container = get_and_verify_password(); if (pwd_container) { uint64_t height; if (!epee::string_tools::get_xtype_from_string(height, args[1])) { fail_msg_writer() << tr("Invalid height"); return true; } m_wallet->set_refresh_from_block_height(height); m_wallet->rewrite(m_wallet_file, pwd_container->password()); } return true; } bool simple_wallet::help(const std::vector &args/* = std::vector()*/) { if(args.empty()) { success_msg_writer() << get_commands_str(); } else { success_msg_writer() << get_command_usage(args); } return true; } simple_wallet::simple_wallet() : m_allow_mismatched_daemon_version(false) , m_refresh_progress_reporter(*this) , m_idle_run(true) , m_auto_refresh_enabled(false) , m_auto_refresh_refreshing(false) , m_in_manual_refresh(false) , m_current_subaddress_account(0) { m_cmd_binder.set_handler("start_mining", boost::bind(&simple_wallet::start_mining, this, _1), tr("start_mining [] [bg_mining] [ignore_battery]"), tr("Start mining in the daemon (bg_mining and ignore_battery are optional booleans).")); m_cmd_binder.set_handler("stop_mining", boost::bind(&simple_wallet::stop_mining, this, _1), tr("Stop mining in the daemon.")); m_cmd_binder.set_handler("set_daemon", boost::bind(&simple_wallet::set_daemon, this, _1), tr("set_daemon [:]"), tr("Set another daemon to connect to.")); m_cmd_binder.set_handler("save_bc", boost::bind(&simple_wallet::save_bc, this, _1), tr("Save the current blockchain data.")); m_cmd_binder.set_handler("refresh", boost::bind(&simple_wallet::refresh, this, _1), tr("Synchronize the transactions and balance.")); m_cmd_binder.set_handler("balance", boost::bind(&simple_wallet::show_balance, this, _1), tr("balance [detail]"), tr("Show the wallet's balance of the currently selected account.")); m_cmd_binder.set_handler("incoming_transfers", boost::bind(&simple_wallet::show_incoming_transfers, this, _1), tr("incoming_transfers [available|unavailable] [verbose] [index=[,[,...]]]"), tr("Show the incoming transfers, all or filtered by availability and address index.")); m_cmd_binder.set_handler("payments", boost::bind(&simple_wallet::show_payments, this, _1), tr("payments [ ... ]"), tr("Show the payments for the given payment IDs.")); m_cmd_binder.set_handler("bc_height", boost::bind(&simple_wallet::show_blockchain_height, this, _1), tr("Show the blockchain height.")); m_cmd_binder.set_handler("transfer_original", boost::bind(&simple_wallet::transfer, this, _1), tr("transfer_original [index=[,,...]] [] []
[]"), tr("Transfer to
using an older transaction building algorithm. If the parameter \"index=[,,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. is the priority of the transaction. The higher the priority, the higher the fee of the transaction. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. is the number of inputs to include for untraceability. Multiple payments can be made at once by adding etcetera (before the payment ID, if it's included)")); m_cmd_binder.set_handler("transfer", boost::bind(&simple_wallet::transfer_new, this, _1), tr("transfer [index=[,,...]] [] []
[]"), tr("Transfer to
. If the parameter \"index=[,,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. is the priority of the transaction. The higher the priority, the higher the fee of the transaction. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. is the number of inputs to include for untraceability. Multiple payments can be made at once by adding etcetera (before the payment ID, if it's included)")); m_cmd_binder.set_handler("locked_transfer", boost::bind(&simple_wallet::locked_transfer, this, _1), tr("locked_transfer [index=[,,...]] [] [] []"), tr("Transfer to
and lock it for (max. 1000000). If the parameter \"index=[,,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. is the priority of the transaction. The higher the priority, the higher the fee of the transaction. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. is the number of inputs to include for untraceability. Multiple payments can be made at once by adding etcetera (before the payment ID, if it's included)")); m_cmd_binder.set_handler("sweep_unmixable", boost::bind(&simple_wallet::sweep_unmixable, this, _1), tr("Send all unmixable outputs to yourself with ring_size 1")); m_cmd_binder.set_handler("sweep_all", boost::bind(&simple_wallet::sweep_all, this, _1), tr("sweep_all [index=[,,...]] [] []
[]"), tr("Send all unlocked balance to an address. If the parameter \"index[,,...]\" is specified, the wallet sweeps outputs received by those address indices. If omitted, the wallet randomly chooses an address index to be used.")); m_cmd_binder.set_handler("sweep_below", boost::bind(&simple_wallet::sweep_below, this, _1), tr("sweep_below [index=[,,...]] [] []
[]"), tr("Send all unlocked outputs below the threshold to an address.")); m_cmd_binder.set_handler("sweep_single", boost::bind(&simple_wallet::sweep_single, this, _1), tr("sweep_single [] []
[]"), tr("Send a single output of the given key image to an address without change.")); m_cmd_binder.set_handler("donate", boost::bind(&simple_wallet::donate, this, _1), tr("donate [index=[,,...]] [] [] []"), tr("Donate to the development team (donate.getmonero.org).")); m_cmd_binder.set_handler("sign_transfer", boost::bind(&simple_wallet::sign_transfer, this, _1), tr("sign_transfer "), tr("Sign a transaction from a .")); m_cmd_binder.set_handler("submit_transfer", boost::bind(&simple_wallet::submit_transfer, this, _1), tr("Submit a signed transaction from a file.")); m_cmd_binder.set_handler("set_log", boost::bind(&simple_wallet::set_log, this, _1), tr("set_log |{+,-,}"), tr("Change the current log detail (level must be <0-4>).")); m_cmd_binder.set_handler("account", boost::bind(&simple_wallet::account, this, _1), tr("account [new