Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent dust outputs from being created during withdraw from wallet #4093

Merged
merged 4 commits into from Apr 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/src/main/resources/i18n/displayStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ shared.noDateAvailable=No date available
shared.noDetailsAvailable=No details available
shared.notUsedYet=Not used yet
shared.date=Date
shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired transaction fee is: {3} ({4} satoshis/byte)\nTransaction size: {5} Kb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount?
shared.sendFundsDetailsWithFee=Sending: {0}\nFrom address: {1}\nTo receiving address: {2}.\nRequired mining fee is: {3} ({4} satoshis/byte)\nTransaction size: {5} Kb\n\nThe recipient will receive: {6}\n\nAre you sure you want to withdraw this amount?
shared.sendFundsDetailsDust=Bisq detected that this transaction would create a change output which is below the minimum dust threshold (and not allowed by Bitcoin consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n
shared.copyToClipboard=Copy to clipboard
shared.language=Language
shared.country=Country
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.wallet.Wallet;

import javax.inject.Inject;
Expand Down Expand Up @@ -97,6 +98,8 @@

import org.spongycastle.crypto.params.KeyParameter;

import java.text.MessageFormat;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
Expand Down Expand Up @@ -330,25 +333,52 @@ private void onWithdraw() {
feeEstimationTransaction = walletService.getFeeEstimationTransactionForMultipleAddresses(fromAddresses, sendersAmount);
}
checkNotNull(feeEstimationTransaction, "feeEstimationTransaction must not be null");
Coin fee = feeEstimationTransaction.getFee();
sendersAmount = feeExcluded ? amountAsCoin.add(fee) : amountAsCoin;
Coin receiverAmount = feeExcluded ? amountAsCoin : amountAsCoin.subtract(fee);

Coin dust = getDust(feeEstimationTransaction);
Coin fee = feeEstimationTransaction.getFee().add(dust);
Coin receiverAmount = Coin.ZERO;
// amountAsCoin is what the user typed into the withdrawal field.
// this can be interpreted as either the senders amount or receivers amount depending
// on a radio button "fee excluded / fee included".
// therefore we calculate the actual sendersAmount and receiverAmount as follows:
if (feeExcluded) {
receiverAmount = amountAsCoin;
sendersAmount = receiverAmount.add(fee);
} else {
sendersAmount = amountAsCoin.add(dust); // sendersAmount bumped up to UTXO size when dust is in play
receiverAmount = sendersAmount.subtract(fee);
}
if (dust.isPositive()) {
log.info("Dust output ({} satoshi) was detected, the dust amount has been added to the fee (was {}, now {})",
dust.value,
feeEstimationTransaction.getFee(),
fee.value);
}

if (areInputsValid()) {
int txSize = feeEstimationTransaction.bitcoinSerialize().length;
log.info("Fee for tx with size {}: {} " + Res.getBaseCurrencyCode() + "", txSize, fee.toPlainString());

if (receiverAmount.isPositive()) {
double feePerByte = CoinUtil.getFeePerByte(fee, txSize);
double kb = txSize / 1000d;

String messageText = Res.get("shared.sendFundsDetailsWithFee",
formatter.formatCoinWithCode(sendersAmount),
withdrawFromTextField.getText(),
withdrawToTextField.getText(),
formatter.formatCoinWithCode(fee),
feePerByte,
kb,
formatter.formatCoinWithCode(receiverAmount));
if (dust.isPositive()) {
messageText = Res.get("shared.sendFundsDetailsDust",
dust.value, dust.value > 1 ? "s" : "")
+ messageText;
}

new Popup().headLine(Res.get("funds.withdrawal.confirmWithdrawalRequest"))
.confirmation(Res.get("shared.sendFundsDetailsWithFee",
formatter.formatCoinWithCode(sendersAmount),
withdrawFromTextField.getText(),
withdrawToTextField.getText(),
formatter.formatCoinWithCode(fee),
feePerByte,
kb,
formatter.formatCoinWithCode(receiverAmount)))
.confirmation(messageText)
.actionButtonText(Res.get("shared.yes"))
.onAction(() -> doWithdraw(sendersAmount, fee, new FutureCallback<>() {
@Override
Expand Down Expand Up @@ -629,6 +659,21 @@ public void updateItem(final WithdrawalListItem item, boolean empty) {
}
});
}

// BISQ issue #4039: prevent dust outputs from being created.
// check the outputs of a proposed transaction, if any are below the dust threshold
// add up the dust, noting the details in the log.
// returns the 'dust amount' to indicate if any dust was detected.
private Coin getDust(Transaction transaction) {
Coin dust = Coin.ZERO;
for (TransactionOutput transactionOutput: transaction.getOutputs()) {
if (transactionOutput.getValue().isLessThan(Restrictions.getMinNonDustOutput())) {
dust = dust.add(transactionOutput.getValue());
log.info("dust TXO = {}", transactionOutput.toString());
}
}
return dust;
}
}