From 3204f0d53642588643312a742d13cc2b1d1e3cd9 Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Sat, 30 May 2015 09:13:52 +0100 Subject: [PATCH 1/2] wallet: add a sweep_dust command Sends all the dust to your own wallet. May fail (if the fee required is more than the dust total). May end up paying most of the dust in fees. Unlocked dust total is now also displayed in "balance". --- src/simplewallet/simplewallet.cpp | 129 +++++++++++++++++- src/simplewallet/simplewallet.h | 1 + src/wallet/wallet2.cpp | 211 ++++++++++++++++++++++++++++++ src/wallet/wallet2.h | 4 + 4 files changed, 344 insertions(+), 1 deletion(-) diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index e47938357..33224aa19 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -295,6 +295,7 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("payments", boost::bind(&simple_wallet::show_payments, this, _1), "payments [ ... ] - Show payments , ... "); m_cmd_binder.set_handler("bc_height", boost::bind(&simple_wallet::show_blockchain_height, this, _1), "Show blockchain height"); m_cmd_binder.set_handler("transfer", boost::bind(&simple_wallet::transfer, this, _1), "transfer [] [ ... ] [payment_id] - Transfer ,... to ,... , respectively. is the number of transactions yours is indistinguishable from (from 0 to maximum available)"); + m_cmd_binder.set_handler("sweep_dust", boost::bind(&simple_wallet::sweep_dust, this, _1), "Send all dust outputs to the same address with mixin 0"); m_cmd_binder.set_handler("set_log", boost::bind(&simple_wallet::set_log, this, _1), "set_log - Change current log detalization level, is a number 0-4"); m_cmd_binder.set_handler("address", boost::bind(&simple_wallet::print_address, this, _1), "Show current wallet public address"); m_cmd_binder.set_handler("save", boost::bind(&simple_wallet::save, this, _1), "Save wallet synchronized data"); @@ -942,7 +943,8 @@ bool simple_wallet::refresh(const std::vector& args) //---------------------------------------------------------------------------------------------------- bool simple_wallet::show_balance(const std::vector& args/* = std::vector()*/) { - success_msg_writer() << "balance: " << print_money(m_wallet->balance()) << ", unlocked balance: " << print_money(m_wallet->unlocked_balance()); + success_msg_writer() << "balance: " << print_money(m_wallet->balance()) << ", unlocked balance: " << print_money(m_wallet->unlocked_balance()) + << ", including unlocked dust: " << print_money(m_wallet->unlocked_dust_balance(tools::tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD))); return true; } //---------------------------------------------------------------------------------------------------- @@ -1307,6 +1309,131 @@ bool simple_wallet::transfer(const std::vector &args_) return true; } + +//---------------------------------------------------------------------------------------------------- +bool simple_wallet::sweep_dust(const std::vector &args_) +{ + if (!try_connect_to_daemon()) + return true; + + try + { + uint64_t total_dust = m_wallet->unlocked_dust_balance(tools::tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD)); + + // figure out what tx will be necessary + auto ptx_vector = m_wallet->create_dust_sweep_transactions(); + + // give user total and fee, and prompt to confirm + uint64_t total_fee = 0; + for (size_t n = 0; n < ptx_vector.size(); ++n) + { + total_fee += ptx_vector[n].fee; + } + + std::string prompt_str = "Sweeping " + print_money(total_dust); + if (ptx_vector.size() > 1) { + prompt_str += " in "; + prompt_str += std::to_string(ptx_vector.size()); + prompt_str += " transactions"; + } + prompt_str += " for a total fee of " + print_money(total_fee); + prompt_str += ". Is this okay? (Y/Yes/N/No)"; + std::string accepted = command_line::input_line(prompt_str); + if (accepted != "Y" && accepted != "y" && accepted != "Yes" && accepted != "yes") + { + fail_msg_writer() << "Transaction cancelled."; + + // would like to return false, because no tx made, but everything else returns true + // and I don't know what returning false might adversely affect. *sigh* + return true; + } + + // actually commit the transactions + while (!ptx_vector.empty()) + { + auto & ptx = ptx_vector.back(); + m_wallet->commit_tx(ptx); + success_msg_writer(true) << "Money successfully sent, transaction " << get_transaction_hash(ptx.tx); + + // if no exception, remove element from vector + ptx_vector.pop_back(); + } + } + catch (const tools::error::daemon_busy&) + { + fail_msg_writer() << "daemon is busy. Please try later"; + } + catch (const tools::error::no_connection_to_daemon&) + { + fail_msg_writer() << "no connection to daemon. Please, make sure daemon is running."; + } + catch (const tools::error::wallet_rpc_error& e) + { + LOG_ERROR("Unknown RPC error: " << e.to_string()); + fail_msg_writer() << "RPC error \"" << e.what() << '"'; + } + catch (const tools::error::get_random_outs_error&) + { + fail_msg_writer() << "failed to get random outputs to mix"; + } + catch (const tools::error::not_enough_money& e) + { + fail_msg_writer() << "not enough money to transfer, available only " << print_money(e.available()) << + ", transaction amount " << print_money(e.tx_amount() + e.fee()) << " = " << print_money(e.tx_amount()) << + " + " << print_money(e.fee()) << " (fee)"; + } + catch (const tools::error::not_enough_outs_to_mix& e) + { + auto writer = fail_msg_writer(); + writer << "not enough outputs for specified mixin_count = " << e.mixin_count() << ":"; + for (const cryptonote::COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& outs_for_amount : e.scanty_outs()) + { + writer << "\noutput amount = " << print_money(outs_for_amount.amount) << ", fount outputs to mix = " << outs_for_amount.outs.size(); + } + } + catch (const tools::error::tx_not_constructed&) + { + fail_msg_writer() << "transaction was not constructed"; + } + catch (const tools::error::tx_rejected& e) + { + fail_msg_writer() << "transaction " << get_transaction_hash(e.tx()) << " was rejected by daemon with status \"" << e.status() << '"'; + } + catch (const tools::error::tx_sum_overflow& e) + { + fail_msg_writer() << e.what(); + } + catch (const tools::error::zero_destination&) + { + fail_msg_writer() << "one of destinations is zero"; + } + catch (const tools::error::tx_too_big& e) + { + fail_msg_writer() << "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() << "unknown transfer error: " << e.what(); + } + catch (const tools::error::wallet_internal_error& e) + { + LOG_ERROR("internal error: " << e.to_string()); + fail_msg_writer() << "internal error: " << e.what(); + } + catch (const std::exception& e) + { + LOG_ERROR("unexpected error: " << e.what()); + fail_msg_writer() << "unexpected error: " << e.what(); + } + catch (...) + { + LOG_ERROR("Unknown error"); + fail_msg_writer() << "unknown error"; + } + + return true; +} //---------------------------------------------------------------------------------------------------- bool simple_wallet::run() { diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index de5b3674e..d4ef0703b 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -103,6 +103,7 @@ namespace cryptonote bool show_payments(const std::vector &args); bool show_blockchain_height(const std::vector &args); bool transfer(const std::vector &args); + bool sweep_dust(const std::vector &args); std::vector> split_amounts( std::vector dsts, size_t num_splits ); diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 5ff8ae408..a3deb5ac5 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -1202,6 +1202,217 @@ std::vector wallet2::create_transactions(std::vector selected_transfers; + for (transfer_container::const_iterator i = m_transfers.begin(); i != m_transfers.end(); ++i) + { + const transfer_details& td = *i; + if (!td.m_spent && td.amount() < dust_policy.dust_threshold && is_transfer_unlocked(td)) + { + money += td.amount(); + } + } + return money; +} + +template +void wallet2::transfer_dust(size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector &extra, cryptonote::transaction& tx, pending_tx &ptx) +{ + using namespace cryptonote; + + // select all dust inputs for transaction + // throw if there are none + uint64_t money = 0; + std::list selected_transfers; + for (transfer_container::iterator i = m_transfers.begin(); i != m_transfers.end(); ++i) + { + const transfer_details& td = *i; + if (!td.m_spent && td.amount() < dust_policy.dust_threshold && is_transfer_unlocked(td)) + { + selected_transfers.push_back (i); + money += td.amount(); + if (selected_transfers.size() >= num_outputs) + break; + } + } + + // we don't allow no output to self, easier, but one may want to burn the dust if = fee + THROW_WALLET_EXCEPTION_IF(money <= needed_fee, error::not_enough_money, money, needed_fee, needed_fee); + + typedef cryptonote::tx_source_entry::output_entry tx_output_entry; + + //prepare inputs + size_t i = 0; + std::vector sources; + BOOST_FOREACH(transfer_container::iterator it, selected_transfers) + { + sources.resize(sources.size()+1); + cryptonote::tx_source_entry& src = sources.back(); + transfer_details& td = *it; + src.amount = td.amount(); + + //paste real transaction to the random index + auto it_to_insert = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) + { + return a.first >= td.m_global_output_index; + }); + tx_output_entry real_oe; + real_oe.first = td.m_global_output_index; + real_oe.second = boost::get(td.m_tx.vout[td.m_internal_output_index].target).key; + auto interted_it = src.outputs.insert(it_to_insert, real_oe); + src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx); + src.real_output = interted_it - src.outputs.begin(); + src.real_output_in_tx_index = td.m_internal_output_index; + detail::print_source_entry(src); + ++i; + } + + cryptonote::tx_destination_entry change_dts = AUTO_VAL_INIT(change_dts); + + std::vector dsts; + uint64_t money_back = money - needed_fee; + if (dust_policy.dust_threshold > 0) + money_back = money_back - money_back % dust_policy.dust_threshold; + dsts.push_back(cryptonote::tx_destination_entry(money_back, m_account_public_address)); + uint64_t dust = 0; + std::vector splitted_dsts; + destination_split_strategy(dsts, change_dts, dust_policy.dust_threshold, splitted_dsts, dust); + THROW_WALLET_EXCEPTION_IF(dust_policy.dust_threshold < dust, error::wallet_internal_error, "invalid dust value: dust = " + + std::to_string(dust) + ", dust_threshold = " + std::to_string(dust_policy.dust_threshold)); + + bool r = cryptonote::construct_tx(m_account.get_keys(), sources, splitted_dsts, extra, tx, unlock_time); + THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, splitted_dsts, unlock_time, m_testnet); + THROW_WALLET_EXCEPTION_IF(m_upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, m_upper_transaction_size_limit); + + std::string key_images; + bool all_are_txin_to_key = std::all_of(tx.vin.begin(), tx.vin.end(), [&](const txin_v& s_e) -> bool + { + CHECKED_GET_SPECIFIC_VARIANT(s_e, const txin_to_key, in, false); + key_images += boost::to_string(in.k_image) + " "; + return true; + }); + THROW_WALLET_EXCEPTION_IF(!all_are_txin_to_key, error::unexpected_txin_type, tx); + + ptx.key_images = key_images; + ptx.fee = money - money_back; + ptx.dust = dust; + ptx.tx = tx; + ptx.change_dts = change_dts; + ptx.selected_transfers = selected_transfers; +} + +//---------------------------------------------------------------------------------------------------- +std::vector wallet2::create_dust_sweep_transactions() +{ + tx_dust_policy dust_policy(::config::DEFAULT_DUST_THRESHOLD); + + size_t num_dust_outputs = 0; + for (transfer_container::const_iterator i = m_transfers.begin(); i != m_transfers.end(); ++i) + { + const transfer_details& td = *i; + if (!td.m_spent && td.amount() < dust_policy.dust_threshold && is_transfer_unlocked(td)) + { + num_dust_outputs++; + } + } + + // failsafe split attempt counter + size_t attempt_count = 0; + + for(attempt_count = 1; ;attempt_count++) + { + size_t num_outputs_per_tx = (num_dust_outputs + attempt_count - 1) / attempt_count; + + std::vector ptx_vector; + try + { + // for each new tx + for (size_t i=0; i extra; + + // loop until fee is met without increasing tx size to next KB boundary. + uint64_t needed_fee = 0; + if (1) + { + transfer_dust(num_outputs_per_tx, (uint64_t)0 /* unlock_time */, 0, detail::digit_split_strategy, dust_policy, extra, tx, ptx); + auto txBlob = t_serializable_object_to_blob(ptx.tx); + uint64_t txSize = txBlob.size(); + uint64_t numKB = txSize / 1024; + if (txSize % 1024) + { + numKB++; + } + needed_fee = numKB * FEE_PER_KB; + + // reroll the tx with the actual amount minus the fee + // if there's not enough for the fee, it'll throw + transfer_dust(num_outputs_per_tx, (uint64_t)0 /* unlock_time */, needed_fee, detail::digit_split_strategy, dust_policy, extra, tx, ptx); + txBlob = t_serializable_object_to_blob(ptx.tx); + } + + ptx_vector.push_back(ptx); + + // mark transfers to be used as "spent" + BOOST_FOREACH(transfer_container::iterator it, ptx.selected_transfers) + it->m_spent = true; + } + + // if we made it this far, we've selected our transactions. committing them will mark them spent, + // so this is a failsafe in case they don't go through + // unmark pending tx transfers as spent + for (auto & ptx : ptx_vector) + { + // mark transfers to be used as not spent + BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + it2->m_spent = false; + + } + + // if we made it this far, we're OK to actually send the transactions + return ptx_vector; + + } + // only catch this here, other exceptions need to pass through to the calling function + catch (const tools::error::tx_too_big& e) + { + + // unmark pending tx transfers as spent + for (auto & ptx : ptx_vector) + { + // mark transfers to be used as not spent + BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + it2->m_spent = false; + + } + + if (attempt_count >= MAX_SPLIT_ATTEMPTS) + { + throw; + } + } + catch (...) + { + // in case of some other exception, make sure any tx in queue are marked unspent again + + // unmark pending tx transfers as spent + for (auto & ptx : ptx_vector) + { + // mark transfers to be used as not spent + BOOST_FOREACH(transfer_container::iterator it2, ptx.selected_transfers) + it2->m_spent = false; + + } + + throw; + } + } +} + //---------------------------------------------------------------------------------------------------- void wallet2::generate_genesis(cryptonote::block& b) { if (m_testnet) diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 712cda40a..a57501786 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -201,15 +201,19 @@ namespace tools uint64_t balance() const; uint64_t unlocked_balance() const; + uint64_t unlocked_dust_balance(const tx_dust_policy &dust_policy) const; template void transfer(const std::vector& dsts, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra, T destination_split_strategy, const tx_dust_policy& dust_policy); template void transfer(const std::vector& dsts, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx& ptx); void transfer(const std::vector& dsts, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra); void transfer(const std::vector& dsts, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector& extra, cryptonote::transaction& tx, pending_tx& ptx); + template + void transfer_dust(size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector& extra, cryptonote::transaction& tx, pending_tx &ptx); void commit_tx(pending_tx& ptx_vector); void commit_tx(std::vector& ptx_vector); std::vector create_transactions(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee, const std::vector extra); + std::vector create_dust_sweep_transactions(); bool check_connection(); void get_transfers(wallet2::transfer_container& incoming_transfers) const; void get_payments(const crypto::hash& payment_id, std::list& payments, uint64_t min_height = 0) const; From a62daebba523395ad4c446c0c0f77752c2b5319e Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Sat, 30 May 2015 15:37:27 +0100 Subject: [PATCH 2/2] wallet_rpc_server: add a sweep_dust RPC command as well --- src/wallet/wallet_rpc_server.cpp | 44 ++++++++++++++++++++ src/wallet/wallet_rpc_server.h | 2 + src/wallet/wallet_rpc_server_commands_defs.h | 18 ++++++++ 3 files changed, 64 insertions(+) diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index e39d9ba7b..9ef19f739 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -273,6 +273,50 @@ namespace tools return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_sweep_dust(const wallet_rpc::COMMAND_RPC_SWEEP_DUST::request& req, wallet_rpc::COMMAND_RPC_SWEEP_DUST::response& res, epee::json_rpc::error& er) + { + if (m_wallet.restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + + try + { + std::vector ptx_vector = m_wallet.create_dust_sweep_transactions(); + + m_wallet.commit_tx(ptx_vector); + + // populate response with tx hashes + for (auto & ptx : ptx_vector) + { + res.tx_hash_list.push_back(boost::lexical_cast(cryptonote::get_transaction_hash(ptx.tx))); + } + + return true; + } + catch (const tools::error::daemon_busy& e) + { + er.code = WALLET_RPC_ERROR_CODE_DAEMON_IS_BUSY; + er.message = e.what(); + return false; + } + catch (const std::exception& e) + { + er.code = WALLET_RPC_ERROR_CODE_GENERIC_TRANSFER_ERROR; + er.message = e.what(); + return false; + } + catch (...) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR"; + return false; + } + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_store(const wallet_rpc::COMMAND_RPC_STORE::request& req, wallet_rpc::COMMAND_RPC_STORE::response& res, epee::json_rpc::error& er) { if (m_wallet.restricted()) diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index cfdc1c147..8538eed2b 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -65,6 +65,7 @@ namespace tools MAP_JON_RPC_WE("getaddress", on_getaddress, wallet_rpc::COMMAND_RPC_GET_ADDRESS) MAP_JON_RPC_WE("transfer", on_transfer, wallet_rpc::COMMAND_RPC_TRANSFER) MAP_JON_RPC_WE("transfer_split", on_transfer_split, wallet_rpc::COMMAND_RPC_TRANSFER_SPLIT) + MAP_JON_RPC_WE("sweep_dust", on_sweep_dust, wallet_rpc::COMMAND_RPC_SWEEP_DUST) MAP_JON_RPC_WE("store", on_store, wallet_rpc::COMMAND_RPC_STORE) MAP_JON_RPC_WE("get_payments", on_get_payments, wallet_rpc::COMMAND_RPC_GET_PAYMENTS) MAP_JON_RPC_WE("get_bulk_payments", on_get_bulk_payments, wallet_rpc::COMMAND_RPC_GET_BULK_PAYMENTS) @@ -79,6 +80,7 @@ namespace tools bool validate_transfer(const std::list destinations, const std::string payment_id, std::vector& dsts, std::vector& extra, epee::json_rpc::error& er); bool on_transfer(const wallet_rpc::COMMAND_RPC_TRANSFER::request& req, wallet_rpc::COMMAND_RPC_TRANSFER::response& res, epee::json_rpc::error& er); bool on_transfer_split(const wallet_rpc::COMMAND_RPC_TRANSFER_SPLIT::request& req, wallet_rpc::COMMAND_RPC_TRANSFER_SPLIT::response& res, epee::json_rpc::error& er); + bool on_sweep_dust(const wallet_rpc::COMMAND_RPC_SWEEP_DUST::request& req, wallet_rpc::COMMAND_RPC_SWEEP_DUST::response& res, epee::json_rpc::error& er); bool on_store(const wallet_rpc::COMMAND_RPC_STORE::request& req, wallet_rpc::COMMAND_RPC_STORE::response& res, epee::json_rpc::error& er); bool on_get_payments(const wallet_rpc::COMMAND_RPC_GET_PAYMENTS::request& req, wallet_rpc::COMMAND_RPC_GET_PAYMENTS::response& res, epee::json_rpc::error& er); bool on_get_bulk_payments(const wallet_rpc::COMMAND_RPC_GET_BULK_PAYMENTS::request& req, wallet_rpc::COMMAND_RPC_GET_BULK_PAYMENTS::response& res, epee::json_rpc::error& er); diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index 35783a189..ecb3cc5e9 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -146,6 +146,24 @@ namespace wallet_rpc }; }; + struct COMMAND_RPC_SWEEP_DUST + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::list tx_hash_list; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(tx_hash_list) + END_KV_SERIALIZE_MAP() + }; + }; + struct COMMAND_RPC_STORE { struct request