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;