aboutsummaryrefslogtreecommitdiff
path: root/src/wallet/wallet2.cpp
diff options
context:
space:
mode:
authorjeffro256 <jeffro256@tutanota.com>2023-05-14 11:41:59 -0500
committerjeffro256 <jeffro256@tutanota.com>2024-02-20 17:08:06 -0600
commitb13c5f6669f9612bef7cba18be7189bf6dfa6e61 (patch)
tree12e25936d4682f5c783dffddce0c7e32361a0d41 /src/wallet/wallet2.cpp
parentMerge pull request #9126 (diff)
downloadmonero-b13c5f6669f9612bef7cba18be7189bf6dfa6e61.tar.xz
wallet: feature: transfer amount with fee included
To transfer ~5 XMR to an address such that your balance drops by exactly 5 XMR, provide a `subtractfeefrom` flag to the `transfer` command. For example: transfer 76bDHojqFYiFCCYYtzTveJ8oFtmpNp3X1TgV2oKP7rHmZyFK1RvyE4r8vsJzf7SyNohMnbKT9wbcD3XUTgsZLX8LU5JBCfm 5 subtractfeefrom=all If my walet balance was exactly 30 XMR before this transaction, it will be exactly 25 XMR afterwards and the destination address will receive slightly less than 5 XMR. You can manually select which destinations fund the transaction fee and which ones do not by providing the destination index. For example: transfer 75sr8AAr... 3 74M7W4eg... 4 7AbWqDZ6... 5 subtractfeefrom=0,2 This will drop your balance by exactly 12 XMR including fees and will spread the fee cost proportionally (3:5 ratio) over destinations with addresses `75sr8AAr...` and `7AbWqDZ6...`, respectively. Disclaimer: This feature was paid for by @LocalMonero.
Diffstat (limited to '')
-rw-r--r--src/wallet/wallet2.cpp164
1 files changed, 140 insertions, 24 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 336f4e159..f45acb175 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -9851,7 +9851,7 @@ static uint32_t get_count_above(const std::vector<wallet2::transfer_details> &tr
// This system allows for sending (almost) the entire balance, since it does
// not generate spurious change in all txes, thus decreasing the instantaneous
// usable balance.
-std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices)
+std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, const unique_index_container& subtract_fee_from_outputs)
{
//ensure device is let in NONE mode in any case
hw::device &hwdev = m_account.get_device();
@@ -9862,11 +9862,12 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
std::vector<std::pair<uint32_t, std::vector<size_t>>> unused_transfers_indices_per_subaddr;
std::vector<std::pair<uint32_t, std::vector<size_t>>> unused_dust_indices_per_subaddr;
- uint64_t needed_money;
+ uint64_t needed_money, total_needed_money; // 'needed_money' is the sum of the destination amounts, while 'total_needed_money' includes 'needed_money' plus the fee if not 'subtract_fee_from_outputs'
uint64_t accumulated_fee, accumulated_change;
struct TX {
std::vector<size_t> selected_transfers;
std::vector<cryptonote::tx_destination_entry> dsts;
+ std::vector<bool> dsts_are_fee_subtractable;
cryptonote::transaction tx;
pending_tx ptx;
size_t weight;
@@ -9876,9 +9877,11 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
TX() : weight(0), needed_fee(0) {}
/* Add an output to the transaction.
+ * If merge_destinations is true, when adding a destination with an existing address, to increment the amount of the existing tx output instead of creating a new one
+ * If subtracting_fee is true, when we generate a final list of destinations for transfer_selected[_rct], this destination will be used to fund the tx fee
* Returns True if the output was added, False if there are no more available output slots.
*/
- bool add(const cryptonote::tx_destination_entry &de, uint64_t amount, unsigned int original_output_index, bool merge_destinations, size_t max_dsts) {
+ bool add(const cryptonote::tx_destination_entry &de, uint64_t amount, unsigned int original_output_index, bool merge_destinations, size_t max_dsts, bool subtracting_fee) {
if (merge_destinations)
{
std::vector<cryptonote::tx_destination_entry>::iterator i;
@@ -9888,6 +9891,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
if (dsts.size() >= max_dsts)
return false;
dsts.push_back(de);
+ dsts_are_fee_subtractable.push_back(subtracting_fee);
i = dsts.end() - 1;
i->amount = 0;
}
@@ -9903,13 +9907,67 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
return false;
dsts.push_back(de);
dsts.back().amount = 0;
+ dsts_are_fee_subtractable.push_back(subtracting_fee);
}
THROW_WALLET_EXCEPTION_IF(memcmp(&dsts[original_output_index].addr, &de.addr, sizeof(de.addr)), error::wallet_internal_error, "Mismatched destination address");
dsts[original_output_index].amount += amount;
}
return true;
}
+
+ // Returns destinations adjusted for given fee if subtract_fee_from_outputs is enabled
+ std::vector<cryptonote::tx_destination_entry> get_adjusted_dsts(uint64_t needed_fee) const
+ {
+ uint64_t dest_total = 0;
+ uint64_t subtractable_dest_total = 0;
+ std::vector<size_t> subtractable_indices;
+ subtractable_indices.reserve(dsts.size());
+ for (size_t i = 0; i < dsts.size(); ++i)
+ {
+ dest_total += dsts[i].amount;
+ if (dsts_are_fee_subtractable[i])
+ {
+ subtractable_dest_total += dsts[i].amount;
+ subtractable_indices.push_back(i);
+ }
+ }
+
+ if (subtractable_indices.empty()) // if subtract_fee_from_outputs is not enabled for this tx
+ return dsts;
+
+ THROW_WALLET_EXCEPTION_IF(subtractable_dest_total < needed_fee, error::tx_not_possible,
+ subtractable_dest_total, dest_total, needed_fee);
+
+ std::vector<cryptonote::tx_destination_entry> res = dsts;
+
+ // subtract fees from destinations equally, rounded down, until dust is left where we subtract 1
+ uint64_t subtractable_remaining = needed_fee;
+ auto si_it = subtractable_indices.cbegin();
+ uint64_t amount_to_subtract = 0;
+ while (subtractable_remaining)
+ {
+ // Set the amount to subtract iterating at the beginning of the list so equal amounts are
+ // subtracted throughout the list of destinations. We use max(x, 1) so that we we still step
+ // forwards even when the amount remaining is less than the number of subtractable indices
+ if (si_it == subtractable_indices.cbegin())
+ amount_to_subtract = std::max<uint64_t>(subtractable_remaining / subtractable_indices.size(), 1);
+
+ cryptonote::tx_destination_entry& d = res[*si_it];
+ THROW_WALLET_EXCEPTION_IF(d.amount <= amount_to_subtract, error::zero_amount);
+
+ subtractable_remaining -= amount_to_subtract;
+ d.amount -= amount_to_subtract;
+ ++si_it;
+
+ // Wrap around to first subtractable index once we hit the end of the list
+ if (si_it == subtractable_indices.cend())
+ si_it = subtractable_indices.cbegin();
+ }
+
+ return res;
+ }
};
+
std::vector<TX> txes;
bool adding_fee; // true if new outputs go towards fee, rather than destinations
uint64_t needed_fee, available_for_fee = 0;
@@ -9932,6 +9990,14 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// throw if attempting a transaction with no destinations
THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination);
+ // throw if subtract_fee_from_outputs has a bad index
+ THROW_WALLET_EXCEPTION_IF(subtract_fee_from_outputs.size() && *subtract_fee_from_outputs.crbegin() >= dsts.size(),
+ error::subtract_fee_from_bad_index, *subtract_fee_from_outputs.crbegin());
+
+ // throw if subtract_fee_from_outputs is enabled and we have too many outputs to fit into one tx
+ THROW_WALLET_EXCEPTION_IF(subtract_fee_from_outputs.size() && dsts.size() > BULLETPROOF_MAX_OUTPUTS - 1,
+ error::wallet_internal_error, "subtractfeefrom transfers cannot be split over multiple transactions yet");
+
// calculate total amount being sent to all destinations
// throw if total amount overflows uint64_t
needed_money = 0;
@@ -9959,6 +10025,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// we could also check for being within FEE_PER_KB, but if the fee calculation
// ever changes, this might be missed, so let this go through
const uint64_t min_fee = (base_fee * estimate_tx_size(use_rct, 1, fake_outs_count, 2, extra.size(), bulletproof, clsag, bulletproof_plus, use_view_tags));
+ total_needed_money = needed_money + (subtract_fee_from_outputs.size() ? 0 : min_fee);
uint64_t balance_subtotal = 0;
uint64_t unlocked_balance_subtotal = 0;
for (uint32_t index_minor : subaddr_indices)
@@ -9966,10 +10033,10 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
balance_subtotal += balance_per_subaddr[index_minor];
unlocked_balance_subtotal += unlocked_balance_per_subaddr[index_minor].first;
}
- THROW_WALLET_EXCEPTION_IF(needed_money + min_fee > balance_subtotal, error::not_enough_money,
+ THROW_WALLET_EXCEPTION_IF(total_needed_money > balance_subtotal || min_fee > balance_subtotal, error::not_enough_money,
balance_subtotal, needed_money, 0);
// first check overall balance is enough, then unlocked one, so we throw distinct exceptions
- THROW_WALLET_EXCEPTION_IF(needed_money + min_fee > unlocked_balance_subtotal, error::not_enough_unlocked_money,
+ THROW_WALLET_EXCEPTION_IF(total_needed_money > unlocked_balance_subtotal || min_fee > unlocked_balance_subtotal, error::not_enough_unlocked_money,
unlocked_balance_subtotal, needed_money, 0);
for (uint32_t i : subaddr_indices)
@@ -10072,7 +10139,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// this is used to build a tx that's 1 or 2 inputs, and 2 outputs, which
// will get us a known fee.
uint64_t estimated_fee = estimate_fee(use_per_byte_fee, use_rct, 2, fake_outs_count, 2, extra.size(), bulletproof, clsag, bulletproof_plus, use_view_tags, base_fee, fee_quantization_mask);
- preferred_inputs = pick_preferred_rct_inputs(needed_money + estimated_fee, subaddr_account, subaddr_indices);
+ total_needed_money = needed_money + (subtract_fee_from_outputs.size() ? 0 : estimated_fee);
+ preferred_inputs = pick_preferred_rct_inputs(total_needed_money, subaddr_account, subaddr_indices);
if (!preferred_inputs.empty())
{
string s;
@@ -10105,7 +10173,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// - we have something to send
// - or we need to gather more fee
// - or we have just one input in that tx, which is rct (to try and make all/most rct txes 2/2)
- unsigned int original_output_index = 0;
+ unsigned int original_output_index = 0, destination_index = 0;
std::vector<size_t>* unused_transfers_indices = &unused_transfers_indices_per_subaddr[0].second;
std::vector<size_t>* unused_dust_indices = &unused_dust_indices_per_subaddr[0].second;
@@ -10188,7 +10256,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// we can fully pay that destination
LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_nettype, dsts[0].is_subaddress, dsts[0].addr) <<
" for " << print_money(dsts[0].amount));
- if (!tx.add(dsts[0], dsts[0].amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1))
+ const bool subtract_fee_from_this_dest = subtract_fee_from_outputs.count(destination_index);
+ if (!tx.add(dsts[0], dsts[0].amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1, subtract_fee_from_this_dest))
{
LOG_PRINT_L2("Didn't pay: ran out of output slots");
out_slots_exhausted = true;
@@ -10198,6 +10267,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
dsts[0].amount = 0;
pop_index(dsts, 0);
++original_output_index;
+ ++destination_index;
}
if (!out_slots_exhausted && available_amount > 0 && !dsts.empty() &&
@@ -10205,7 +10275,8 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// we can partially fill that destination
LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_nettype, dsts[0].is_subaddress, dsts[0].addr) <<
" for " << print_money(available_amount) << "/" << print_money(dsts[0].amount));
- if (tx.add(dsts[0], available_amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1))
+ const bool subtract_fee_from_this_dest = subtract_fee_from_outputs.count(destination_index);
+ if (tx.add(dsts[0], available_amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1, subtract_fee_from_this_dest))
{
dsts[0].amount -= available_amount;
available_amount = 0;
@@ -10284,9 +10355,13 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// Try to carve the estimated fee from the partial payment (if there is one)
available_for_fee = try_carving_from_partial_payment(needed_fee, available_for_fee);
- uint64_t inputs = 0, outputs = needed_fee;
+ uint64_t inputs = 0, outputs = 0;
for (size_t idx: tx.selected_transfers) inputs += m_transfers[idx].amount();
for (const auto &o: tx.dsts) outputs += o.amount;
+ if (subtract_fee_from_outputs.empty()) // if normal tx that doesn't subtract fees
+ {
+ outputs += needed_fee;
+ }
if (inputs < outputs)
{
@@ -10297,15 +10372,32 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
LOG_PRINT_L2("Trying to create a tx now, with " << tx.dsts.size() << " outputs and " <<
tx.selected_transfers.size() << " inputs");
+ auto tx_dsts = tx.get_adjusted_dsts(needed_fee);
if (use_rct)
- transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra,
+ transfer_selected_rct(tx_dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra,
test_tx, test_ptx, rct_config, use_view_tags);
else
- transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra,
+ transfer_selected(tx_dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra,
detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx, use_view_tags);
auto txBlob = t_serializable_object_to_blob(test_ptx.tx);
needed_fee = calculate_fee(use_per_byte_fee, test_ptx.tx, txBlob.size(), base_fee, fee_quantization_mask);
- available_for_fee = test_ptx.fee + test_ptx.change_dts.amount + (!test_ptx.dust_added_to_fee ? test_ptx.dust : 0);
+
+ // Depending on the mode, we take extra fees from either our change output or the destination outputs for which subtract_fee_from_outputs is true
+ uint64_t output_available_for_fee = 0;
+ bool tx_has_subtractable_output = false;
+ for (size_t di = 0; di < tx.dsts.size(); ++di)
+ {
+ if (tx.dsts_are_fee_subtractable[di])
+ {
+ output_available_for_fee += tx.dsts[di].amount;
+ tx_has_subtractable_output = true;
+ }
+ }
+ if (!tx_has_subtractable_output)
+ {
+ output_available_for_fee = test_ptx.change_dts.amount;
+ }
+ available_for_fee = test_ptx.fee + output_available_for_fee + (!test_ptx.dust_added_to_fee ? test_ptx.dust : 0);
LOG_PRINT_L2("Made a " << get_weight_string(test_ptx.tx, txBlob.size()) << " tx, with " << print_money(available_for_fee) << " available for fee (" <<
print_money(needed_fee) << " needed)");
@@ -10321,18 +10413,24 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
else
{
LOG_PRINT_L2("We made a tx, adjusting fee and saving it, we need " << print_money(needed_fee) << " and we have " << print_money(test_ptx.fee));
- do {
+ size_t fee_tries;
+ for (fee_tries = 0; fee_tries < 10 && needed_fee > test_ptx.fee; ++fee_tries) {
+ tx_dsts = tx.get_adjusted_dsts(needed_fee);
+
if (use_rct)
- transfer_selected_rct(tx.dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra,
+ transfer_selected_rct(tx_dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra,
test_tx, test_ptx, rct_config, use_view_tags);
else
- transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra,
+ transfer_selected(tx_dsts, tx.selected_transfers, fake_outs_count, outs, valid_public_keys_cache, unlock_time, needed_fee, extra,
detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx, use_view_tags);
txBlob = t_serializable_object_to_blob(test_ptx.tx);
needed_fee = calculate_fee(use_per_byte_fee, test_ptx.tx, txBlob.size(), base_fee, fee_quantization_mask);
LOG_PRINT_L2("Made an attempt at a final " << get_weight_string(test_ptx.tx, txBlob.size()) << " tx, with " << print_money(test_ptx.fee) <<
" fee and " << print_money(test_ptx.change_dts.amount) << " change");
- } while (needed_fee > test_ptx.fee);
+ };
+
+ THROW_WALLET_EXCEPTION_IF(fee_tries == 10, error::wallet_internal_error,
+ "Too many attempts to raise pending tx fee to level of needed fee");
LOG_PRINT_L2("Made a final " << get_weight_string(test_ptx.tx, txBlob.size()) << " tx, with " << print_money(test_ptx.fee) <<
" fee and " << print_money(test_ptx.change_dts.amount) << " change");
@@ -10385,10 +10483,13 @@ skip_tx:
for (std::vector<TX>::iterator i = txes.begin(); i != txes.end(); ++i)
{
TX &tx = *i;
+
+ const auto tx_dsts = tx.get_adjusted_dsts(tx.needed_fee);
+
cryptonote::transaction test_tx;
pending_tx test_ptx;
if (use_rct) {
- transfer_selected_rct(tx.dsts, /* NOMOD std::vector<cryptonote::tx_destination_entry> dsts,*/
+ transfer_selected_rct(tx_dsts, /* NOMOD std::vector<cryptonote::tx_destination_entry> dsts,*/
tx.selected_transfers, /* const std::list<size_t> selected_transfers */
fake_outs_count, /* CONST size_t fake_outputs_count, */
tx.outs, /* MOD std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, */
@@ -10401,7 +10502,7 @@ skip_tx:
rct_config,
use_view_tags); /* const bool use_view_tags */
} else {
- transfer_selected(tx.dsts,
+ transfer_selected(tx_dsts,
tx.selected_transfers,
fake_outs_count,
tx.outs,
@@ -10435,23 +10536,38 @@ skip_tx:
ptx_vector.push_back(tx.ptx);
}
- THROW_WALLET_EXCEPTION_IF(!sanity_check(ptx_vector, original_dsts), error::wallet_internal_error, "Created transaction(s) failed sanity check");
+ THROW_WALLET_EXCEPTION_IF(!sanity_check(ptx_vector, original_dsts, subtract_fee_from_outputs), error::wallet_internal_error, "Created transaction(s) failed sanity check");
// if we made it this far, we're OK to actually send the transactions
return ptx_vector;
}
-bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, std::vector<cryptonote::tx_destination_entry> dsts) const
+bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, const std::vector<cryptonote::tx_destination_entry>& dsts, const unique_index_container& subtract_fee_from_outputs) const
{
- MDEBUG("sanity_check: " << ptx_vector.size() << " txes, " << dsts.size() << " destinations");
+ MDEBUG("sanity_check: " << ptx_vector.size() << " txes, " << dsts.size() << " destinations, subtract_fee_from_outputs " <<
+ (subtract_fee_from_outputs.size() ? "enabled" : "disabled"));
THROW_WALLET_EXCEPTION_IF(ptx_vector.empty(), error::wallet_internal_error, "No transactions");
+ THROW_WALLET_EXCEPTION_IF(!subtract_fee_from_outputs.empty() && ptx_vector.size() != 1,
+ error::wallet_internal_error, "feature subtractfeefrom not supported for split transactions");
+
+ // For destinations from where the fee is subtracted, the required amount has to be at least
+ // target amount - (tx fee / num_subtractable + 1). +1 since fee might not be evenly divisble by
+ // the number of subtractble destinations. For non-subtractable destinations, we need at least
+ // the target amount.
+ const size_t num_subtractable_dests = subtract_fee_from_outputs.size();
+ const uint64_t fee0 = ptx_vector[0].fee;
+ const uint64_t subtractable_fee_deduction = fee0 / std::max<size_t>(num_subtractable_dests, 1) + 1;
// check every party in there does receive at least the required amount
std::unordered_map<account_public_address, std::pair<uint64_t, bool>> required;
- for (const auto &d: dsts)
+ for (size_t i = 0; i < dsts.size(); ++i)
{
- required[d.addr].first += d.amount;
+ const cryptonote::tx_destination_entry& d = dsts[i];
+ const bool dest_is_subtractable = subtract_fee_from_outputs.count(i);
+ const uint64_t fee_deduction = dest_is_subtractable ? subtractable_fee_deduction : 0;
+ const uint64_t required_amount = d.amount - std::min(fee_deduction, d.amount);
+ required[d.addr].first += required_amount;
required[d.addr].second = d.is_subaddress;
}