diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index ce224660..1c7af1bc 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -209,6 +209,7 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("scan_transfers_for_id", boost::bind(&simple_wallet::scan_transfers_for_id, this, _1), "Rescan transfers for tx_id"); m_cmd_binder.set_handler("scan_transfers_for_ki", boost::bind(&simple_wallet::scan_transfers_for_ki, this, _1), "Rescan transfers for key image"); m_cmd_binder.set_handler("print_utxo_distribution", boost::bind(&simple_wallet::print_utxo_distribution, this, _1), "Prints utxo distribution"); + m_cmd_binder.set_handler("sweep_below", boost::bind(&simple_wallet::sweep_below, this, _1), "sweep_below
[payment_id] - Tries to transfers all coins with amount below the given limit to the given address"); m_cmd_binder.set_handler("address", boost::bind(&simple_wallet::print_address, this, _1), "Show current wallet public address"); m_cmd_binder.set_handler("integrated_address", boost::bind(&simple_wallet::integrated_address, this, _1), "integrated_address [| &args) return true; } //---------------------------------------------------------------------------------------------------- +bool simple_wallet::sweep_below(const std::vector &args) +{ + bool r = false; + if (args.size() < 3 || args.size() > 4) + { + fail_msg_writer() << "invalid agruments count: " << args.size() << ", expected 3 or 4"; + return true; + } + + size_t fake_outs_count = 0; + if (!string_tools::get_xtype_from_string(fake_outs_count, args[0])) + { + fail_msg_writer() << "mixin_count should be non-negative integer, got " << args[0]; + return true; + } + + // parse payment_id + currency::payment_id_t payment_id; + if (args.size() == 4) + { + const std::string &payment_id_str = args.back(); + r = parse_payment_id_from_hex_str(payment_id_str, payment_id); + if (!r) + { + fail_msg_writer() << "payment id has invalid format: \"" << payment_id_str << "\", expected hex string"; + return true; + } + } + + currency::account_public_address addr; + currency::payment_id_t integrated_payment_id; + if (!m_wallet->get_transfer_address(args[1], addr, integrated_payment_id)) + { + fail_msg_writer() << "wrong address: " << args[1]; + return true; + } + + // handle integrated payment id + if (!integrated_payment_id.empty()) + { + if (!payment_id.empty()) + { + fail_msg_writer() << "address " << args[1] << " has integrated payment id " << epee::string_tools::buff_to_hex_nodelimer(integrated_payment_id) << + " which is incompatible with payment id " << epee::string_tools::buff_to_hex_nodelimer(payment_id) << " that was already assigned to this transfer"; + return true; + } + + payment_id = integrated_payment_id; // remember integrated payment id as the main payment id + success_msg_writer() << "NOTE: using payment id " << epee::string_tools::buff_to_hex_nodelimer(payment_id) << " from integrated address " << args[1]; + } + + uint64_t amount = 0; + r = currency::parse_amount(amount, args[2]); + if (!r || amount == 0) + { + fail_msg_writer() << "incorrect amount: " << args[2]; + return true; + } + + try + { + uint64_t fee = m_wallet->get_core_runtime_config().tx_default_fee; + size_t outs_total = 0, outs_swept = 0; + uint64_t amount_total = 0, amount_swept = 0; + currency::transaction result_tx = AUTO_VAL_INIT(result_tx); + m_wallet->sweep_below(fake_outs_count, addr, amount, payment_id, fee, outs_total, amount_total, outs_swept, &result_tx); + if (!get_inputs_money_amount(result_tx, amount_swept)) + LOG_ERROR("get_inputs_money_amount failed, tx: " << obj_to_json_str(result_tx)); + + success_msg_writer(false) << outs_swept << " outputs (" << print_money_brief(amount_swept) << " coins) of " << outs_total << " total (" << print_money_brief(amount_total) + << ") below the specified limit of " << print_money_brief(amount) << " were successfully swept"; + success_msg_writer(true) << "tx: " << get_transaction_hash(result_tx) << " size: " << get_object_blobsize(result_tx) << " bytes"; + } + catch (const std::exception& e) + { + LOG_ERROR(e.what()); + fail_msg_writer() << e.what(); + return true; + } + + return true; +} +//---------------------------------------------------------------------------------------------------- #ifdef WIN32 int wmain( int argc, wchar_t* argv_w[ ], wchar_t* envp[ ] ) #else diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 91fccfd3..f8a3a707 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -84,6 +84,7 @@ namespace currency bool save_watch_only(const std::vector &args); bool sign_transfer(const std::vector &args); bool submit_transfer(const std::vector &args); + bool sweep_below(const std::vector &args); bool get_alias_from_daemon(const std::string& alias_name, currency::extra_alias_entry_base& ai); bool get_transfer_address(const std::string& adr_str, currency::account_public_address& addr); diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 115aaa3a..b36a6cb8 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -4386,6 +4386,251 @@ void wallet2::transfer(const construct_tx_param& ctp, print_tx_sent_message(tx, std::string() + "(transfer)", ctp.fee); } +//---------------------------------------------------------------------------------------------------- +void wallet2::sweep_below(size_t fake_outs_count, const currency::account_public_address& destination_addr, uint64_t threshold_amount, const currency::payment_id_t& payment_id, + uint64_t fee, size_t& outs_total, uint64_t& amount_total, size_t& outs_swept, currency::transaction* p_result_tx /* = nullptr */) +{ + bool r = false; + outs_total = 0; + amount_total = 0; + outs_swept = 0; + std::vector selected_transfers; + selected_transfers.reserve(m_transfers.size()); + for (size_t i = 0; i < m_transfers.size(); ++i) + { + const transfer_details& td = m_transfers[i]; + uint64_t amount = td.amount(); + if (amount < threshold_amount && + is_transfer_ready_to_go(td, fake_outs_count)) + { + selected_transfers.push_back(i); + outs_total += 1; + amount_total += amount; + } + } + + // sort by amount descending in order to spend bigger outputs first + std::sort(selected_transfers.begin(), selected_transfers.end(), [this](size_t a, size_t b) { return m_transfers[b].amount() < m_transfers[a].amount(); }); + + WLT_THROW_IF_FALSE_WALLET_CMN_ERR_EX(!selected_transfers.empty(), "No spendable outputs meet the criterion"); + + typedef COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::out_entry out_entry; + typedef currency::tx_source_entry::output_entry tx_output_entry; + + COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::response rpc_get_random_outs_resp = AUTO_VAL_INIT(rpc_get_random_outs_resp); + if (fake_outs_count > 0) + { + COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::request req = AUTO_VAL_INIT(req); + req.use_forced_mix_outs = false; + req.outs_count = fake_outs_count + 1; + for (size_t i : selected_transfers) + req.amounts.push_back(m_transfers[i].amount()); + + r = m_core_proxy->call_COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS(req, rpc_get_random_outs_resp); + + THROW_IF_FALSE_WALLET_EX(r, error::no_connection_to_daemon, "getrandom_outs.bin"); + THROW_IF_FALSE_WALLET_EX(rpc_get_random_outs_resp.status != CORE_RPC_STATUS_BUSY, error::daemon_busy, "getrandom_outs.bin"); + THROW_IF_FALSE_WALLET_EX(rpc_get_random_outs_resp.status == CORE_RPC_STATUS_OK, error::get_random_outs_error, rpc_get_random_outs_resp.status); + WLT_THROW_IF_FALSE_WALLET_INT_ERR_EX(rpc_get_random_outs_resp.outs.size() == selected_transfers.size(), + "daemon returned wrong number of amounts for getrandom_outs.bin: " << rpc_get_random_outs_resp.outs.size() << ", requested: " << selected_transfers.size()); + + std::vector scanty_outs; + for (COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount& amount_outs : rpc_get_random_outs_resp.outs) + { + if (amount_outs.outs.size() < fake_outs_count) + scanty_outs.push_back(amount_outs); + } + THROW_IF_FALSE_WALLET_EX(scanty_outs.empty(), error::not_enough_outs_to_mix, scanty_outs, fake_outs_count); + } + + finalize_tx_param ftp = AUTO_VAL_INIT(ftp); + if (!payment_id.empty()) + set_payment_id_to_tx(ftp.attachments, payment_id); + // put encrypted payer info into the extra + ftp.crypt_address = destination_addr; + currency::tx_payer txp = AUTO_VAL_INIT(txp); + txp.acc_addr = m_account.get_public_address(); + ftp.extra.push_back(txp); + //ftp.flags; + //ftp.multisig_id; + ftp.prepared_destinations; + // ftp.selected_transfers -- needed only at stage of broadcasting or storing unsigned tx + ftp.shuffle = false; + // ftp.sources -- will be filled in try_construct_tx + ftp.spend_pub_key = m_account.get_public_address().m_spend_public_key; // needed for offline signing + ftp.tx_outs_attr = CURRENCY_TO_KEY_OUT_RELAXED; + ftp.unlock_time = 0; + + enum try_construct_result_t {rc_ok = 0, rc_too_few_outputs = 1, rc_too_many_outputs = 2, rc_create_tx_failed = 3 }; + auto try_construct_tx = [this, &selected_transfers, &rpc_get_random_outs_resp, &fake_outs_count, &fee, &destination_addr] + (size_t st_index_upper_boundary, finalize_tx_param& ftp, uint64_t& amount_swept) -> try_construct_result_t + { + // prepare inputs + amount_swept = 0; + ftp.sources.clear(); + ftp.sources.resize(st_index_upper_boundary); + WLT_THROW_IF_FALSE_WALLET_INT_ERR_EX(st_index_upper_boundary <= selected_transfers.size(), "index_upper_boundary = " << st_index_upper_boundary << ", selected_transfers.size() = " << selected_transfers.size()); + for (size_t st_index = 0; st_index < st_index_upper_boundary; ++st_index) + { + currency::tx_source_entry& src = ftp.sources[st_index]; + size_t tr_index = selected_transfers[st_index]; + transfer_details& td = m_transfers[tr_index]; + src.transfer_index = tr_index; + src.amount = td.amount(); + amount_swept += src.amount; + + // populate src.outputs with mix-ins + if (rpc_get_random_outs_resp.outs.size()) + { + // TODO: is the folllowing line neccesary? + rpc_get_random_outs_resp.outs[st_index].outs.sort([](const out_entry& a, const out_entry& b) { return a.global_amount_index < b.global_amount_index; }); + for (out_entry& daemon_oe : rpc_get_random_outs_resp.outs[st_index].outs) + { + if (td.m_global_output_index == daemon_oe.global_amount_index) + continue; + tx_output_entry oe; + oe.first = daemon_oe.global_amount_index; + oe.second = daemon_oe.out_key; + src.outputs.push_back(oe); + if (src.outputs.size() >= fake_outs_count) + break; + } + } + + // insert real output into src.outputs + auto it_to_insert = std::find_if(src.outputs.begin(), src.outputs.end(), [&](const tx_output_entry& a) + { + if (a.first.type().hash_code() == typeid(uint64_t).hash_code()) + return boost::get(a.first) >= td.m_global_output_index; + return false; // TODO: implement deterministics real output placement in case there're ref_by_id outs + }); + tx_output_entry real_oe; + real_oe.first = td.m_global_output_index; + real_oe.second = boost::get(td.m_ptx_wallet_info->m_tx.vout[td.m_internal_output_index].target).key; + auto inserted_it = src.outputs.insert(it_to_insert, real_oe); + src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_ptx_wallet_info->m_tx); + src.real_output = inserted_it - src.outputs.begin(); + src.real_output_in_tx_index = td.m_internal_output_index; + //detail::print_source_entry(src); + } + + if (amount_swept <= fee) + return rc_too_few_outputs; + + // try to construct a transaction + std::vector dsts({ tx_destination_entry(amount_swept - fee, destination_addr) }); + prepare_tx_destinations(0, 0, detail::ssi_digit, tools::tx_dust_policy(), dsts, ftp.prepared_destinations); + + currency::transaction tx = AUTO_VAL_INIT(tx); + crypto::secret_key tx_key = AUTO_VAL_INIT(tx_key); + try + { + finalize_transaction(ftp, tx, tx_key, false, false); + } + catch (error::tx_too_big) + { + return rc_too_many_outputs; + } + catch (...) + { + return rc_create_tx_failed; + } + + return rc_ok; + }; + + static const size_t estimated_bytes_per_input = 78; + const size_t estimated_max_inputs = static_cast(CURRENCY_MAX_TRANSACTION_BLOB_SIZE / (estimated_bytes_per_input * (fake_outs_count + 1.5))); + + size_t st_index_upper_boundary = std::min(selected_transfers.size(), estimated_max_inputs); // selected_transfers.size(); + uint64_t amount_swept = 0; + try_construct_result_t res = try_construct_tx(st_index_upper_boundary, ftp, amount_swept); + + WLT_THROW_IF_FALSE_WALLET_CMN_ERR_EX(res != rc_too_few_outputs, st_index_upper_boundary << " biggest unspent outputs have total amount of " << print_money_brief(amount_swept) + << " which is less than required fee: " << print_money_brief(fee) << ", transaction cannot be constructed"); + + if (res == rc_too_many_outputs) + { + // TODO: add logs + size_t low_bound = 0; + size_t high_bound = st_index_upper_boundary; + finalize_tx_param ftp_ok = ftp; + for (;;) + { + if (low_bound + 1 >= high_bound) + { + st_index_upper_boundary = low_bound; + res = rc_ok; + ftp = ftp_ok; + break; + } + st_index_upper_boundary = (low_bound + high_bound) / 2; + try_construct_result_t res = try_construct_tx(st_index_upper_boundary, ftp, amount_swept); + if (res == rc_ok) + { + low_bound = st_index_upper_boundary; + ftp_ok = ftp; + } + else if (res == rc_too_many_outputs) + { + high_bound = st_index_upper_boundary; + } + else + break; + } + } + + if (res != rc_ok) + { + uint64_t amount_min = UINT64_MAX, amount_max = 0, amount_sum = 0; + for (auto& i : selected_transfers) + { + uint64_t amount = m_transfers[i].amount(); + amount_min = std::min(amount_min, amount); + amount_max = std::max(amount_max, amount); + amount_sum += amount; + } + WLT_THROW_IF_FALSE_WALLET_INT_ERR_EX(false, "try_construct_tx failed with result: " << res << + ", selected_transfers stats:\n" << + " outs: " << selected_transfers.size() << ENDL << + " amount min: " << print_money(amount_min) << ENDL << + " amount max: " << print_money(amount_max) << ENDL << + " amount avg: " << (selected_transfers.empty() ? std::string("n/a") : print_money(amount_sum / selected_transfers.size()))); + } + + // populate ftp.selected_transfers from ftp.sources + ftp.selected_transfers.clear(); + for (size_t i = 0; i < ftp.sources.size(); ++i) + ftp.selected_transfers.push_back(ftp.sources[i].transfer_index); + + outs_swept = ftp.sources.size(); + + + if (m_watch_only) + { + bool r = store_unsigned_tx_to_file_and_reserve_transfers(ftp, "zano_tx_unsigned"); + WLT_THROW_IF_FALSE_WALLET_CMN_ERR_EX(r, "failed to store unsigned tx"); + return; + } + + mark_transfers_as_spent(ftp.selected_transfers, "sweep_below"); + + transaction local_tx; + transaction* p_tx = p_result_tx != nullptr ? p_result_tx : &local_tx; + *p_tx = AUTO_VAL_INIT_T(transaction); + try + { + crypto::secret_key sk = AUTO_VAL_INIT(sk); + finalize_transaction(ftp, *p_tx, sk, true); + } + catch (...) + { + clear_transfers_from_flag(ftp.selected_transfers, WALLET_TRANSFER_DETAIL_FLAG_SPENT, std::string("exception on sweep_below, tx id (might be wrong): ") + epee::string_tools::pod_to_hex(get_transaction_hash(*p_tx))); + throw; + } + + +} } // namespace tools diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 081e041d..60b7185f 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -622,6 +622,9 @@ namespace tools void submit_transfer(const std::string& signed_tx_blob, currency::transaction& tx); void submit_transfer_files(const std::string& signed_tx_file, currency::transaction& tx); + void sweep_below(size_t fake_outs_count, const currency::account_public_address& destination_addr, uint64_t threshold_amount, const currency::payment_id_t& payment_id, + uint64_t fee, size_t& outs_total, uint64_t& amount_total, size_t& outs_swept, currency::transaction* p_result_tx = nullptr); + bool get_transfer_address(const std::string& adr_str, currency::account_public_address& addr, std::string& payment_id); uint64_t get_blockchain_current_height() const { return m_blockchain.size(); }