Bugfix with withdrawal, display exact bitcoin value without rounding

This commit is contained in:
Manfred Karrer 2016-04-28 18:58:14 +02:00
parent a38fb9d21c
commit 24cc6800e7
10 changed files with 99 additions and 55 deletions

View file

@ -24,7 +24,7 @@ public class Restrictions {
public static final Coin MIN_TRADE_AMOUNT = Coin.parseCoin("0.0001"); // 4 cent @ 400 EUR/BTC public static final Coin MIN_TRADE_AMOUNT = Coin.parseCoin("0.0001"); // 4 cent @ 400 EUR/BTC
public static boolean isAboveFixedTxFeeAndDust(Coin amount) { public static boolean isAboveFixedTxFeeForTradesAndDust(Coin amount) {
return amount != null && amount.compareTo(FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT)) > 0; return amount != null && amount.compareTo(FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT)) > 0;
} }

View file

@ -145,7 +145,7 @@ public class TradeWalletService {
boolean useSavingsWallet, Coin tradingFee, String feeReceiverAddresses) boolean useSavingsWallet, Coin tradingFee, String feeReceiverAddresses)
throws InsufficientMoneyException, AddressFormatException { throws InsufficientMoneyException, AddressFormatException {
Transaction tradingFeeTx = new Transaction(params); Transaction tradingFeeTx = new Transaction(params);
Preconditions.checkArgument(Restrictions.isAboveFixedTxFeeAndDust(tradingFee), Preconditions.checkArgument(Restrictions.isAboveFixedTxFeeForTradesAndDust(tradingFee),
"You cannot send an amount which are smaller than the fee + dust output."); "You cannot send an amount which are smaller than the fee + dust output.");
Coin outPutAmount = tradingFee.subtract(FeePolicy.getFixedTxFeeForTrades()); Coin outPutAmount = tradingFee.subtract(FeePolicy.getFixedTxFeeForTrades());
tradingFeeTx.addOutput(outPutAmount, new Address(params, feeReceiverAddresses)); tradingFeeTx.addOutput(outPutAmount, new Address(params, feeReceiverAddresses));

View file

@ -717,7 +717,7 @@ public class WalletService {
AddressEntryException, InsufficientMoneyException { AddressEntryException, InsufficientMoneyException {
Transaction tx = new Transaction(params); Transaction tx = new Transaction(params);
Preconditions.checkArgument(Restrictions.isAboveDust(amount), Preconditions.checkArgument(Restrictions.isAboveDust(amount),
"You cannot send an amount which are smaller than 546 satoshis."); "The amount is too low (dust limit).");
tx.addOutput(amount, new Address(params, toAddress)); tx.addOutput(amount, new Address(params, toAddress));
Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx); Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx);
@ -742,7 +742,7 @@ public class WalletService {
AddressFormatException, AddressEntryException, InsufficientMoneyException { AddressFormatException, AddressEntryException, InsufficientMoneyException {
Transaction tx = new Transaction(params); Transaction tx = new Transaction(params);
Preconditions.checkArgument(Restrictions.isAboveDust(amount), Preconditions.checkArgument(Restrictions.isAboveDust(amount),
"You cannot send an amount which are smaller than 546 satoshis."); "The amount is too low (dust limit).");
tx.addOutput(amount, new Address(params, toAddress)); tx.addOutput(amount, new Address(params, toAddress));
Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx); Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx);

View file

@ -28,21 +28,21 @@ public class RestrictionsTest {
@Test @Test
public void testIsMinSpendableAmount() { public void testIsMinSpendableAmount() {
Coin amount = null; Coin amount = null;
assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeForTradesAndDust(amount));
amount = Coin.ZERO; amount = Coin.ZERO;
assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeForTradesAndDust(amount));
amount = FeePolicy.getFixedTxFeeForTrades(); amount = FeePolicy.getFixedTxFeeForTrades();
assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeForTradesAndDust(amount));
amount = Transaction.MIN_NONDUST_OUTPUT; amount = Transaction.MIN_NONDUST_OUTPUT;
assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeForTradesAndDust(amount));
amount = FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT); amount = FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT);
assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeForTradesAndDust(amount));
amount = FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT).add(Coin.valueOf(1)); amount = FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT).add(Coin.valueOf(1));
assertTrue("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount)); assertTrue("tx unfunded, pending", Restrictions.isAboveFixedTxFeeForTradesAndDust(amount));
} }
} }

View file

@ -161,7 +161,7 @@ public class BitsquareApp extends Application {
mainView.setPersistedFilesCorrupted(corruptedDatabaseFiles); mainView.setPersistedFilesCorrupted(corruptedDatabaseFiles);
});*/ });*/
scene = new Scene(mainView.getRoot(), 1150, 740); scene = new Scene(mainView.getRoot(), 1190, 740);
scene.getStylesheets().setAll( scene.getStylesheets().setAll(
"/io/bitsquare/gui/bitsquare.css", "/io/bitsquare/gui/bitsquare.css",
"/io/bitsquare/gui/images.css"); "/io/bitsquare/gui/images.css");
@ -193,7 +193,7 @@ public class BitsquareApp extends Application {
// configure the primary stage // configure the primary stage
primaryStage.setTitle(env.getRequiredProperty(APP_NAME_KEY)); primaryStage.setTitle(env.getRequiredProperty(APP_NAME_KEY));
primaryStage.setScene(scene); primaryStage.setScene(scene);
primaryStage.setMinWidth(1130); primaryStage.setMinWidth(1170);
primaryStage.setMinHeight(620); primaryStage.setMinHeight(620);
// on windows the title icon is also used as task bar icon in a larger size // on windows the title icon is also used as task bar icon in a larger size

View file

@ -152,7 +152,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
return type != null ? "Market price (" + type.name + ")" : ""; return type != null ? "Market price (" + type.name + ")" : "";
}, },
model.marketPriceCurrency, model.typeProperty)); model.marketPriceCurrency, model.typeProperty));
HBox.setMargin(marketPriceBox.third, new Insets(0, 20, 0, 0)); HBox.setMargin(marketPriceBox.third, new Insets(0, 0, 0, 0));
Tuple2<TextField, VBox> availableBalanceBox = getBalanceBox("Available balance"); Tuple2<TextField, VBox> availableBalanceBox = getBalanceBox("Available balance");
@ -243,7 +243,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
private Tuple2<TextField, VBox> getBalanceBox(String text) { private Tuple2<TextField, VBox> getBalanceBox(String text) {
TextField textField = new TextField(); TextField textField = new TextField();
textField.setEditable(false); textField.setEditable(false);
textField.setPrefWidth(120); textField.setPrefWidth(140);
textField.setMouseTransparent(true); textField.setMouseTransparent(true);
textField.setFocusTraversable(false); textField.setFocusTraversable(false);
textField.setStyle("-fx-alignment: center; -fx-background-color: white;"); textField.setStyle("-fx-alignment: center; -fx-background-color: white;");

View file

@ -292,7 +292,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
private Coin getAmountAsCoin() { private Coin getAmountAsCoin() {
Coin senderAmount = formatter.parseToCoin(amountTextField.getText()); Coin senderAmount = formatter.parseToCoin(amountTextField.getText());
if (!Restrictions.isAboveFixedTxFeeAndDust(senderAmount)) { if (!Restrictions.isAboveFixedTxFeeForTradesAndDust(senderAmount)) {
senderAmount = Coin.ZERO; senderAmount = Coin.ZERO;
/* new Popup() /* new Popup()
.warning("The amount is lower than the transaction fee and the min. possible tx value (dust).") .warning("The amount is lower than the transaction fee and the min. possible tx value (dust).")

View file

@ -22,7 +22,6 @@ import de.jensd.fx.fontawesome.AwesomeIcon;
import io.bitsquare.app.BitsquareApp; import io.bitsquare.app.BitsquareApp;
import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.AddressEntryException; import io.bitsquare.btc.AddressEntryException;
import io.bitsquare.btc.Restrictions;
import io.bitsquare.btc.WalletService; import io.bitsquare.btc.WalletService;
import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.common.UserThread; import io.bitsquare.common.UserThread;
@ -40,8 +39,10 @@ import io.bitsquare.trade.TradeManager;
import io.bitsquare.trade.closed.ClosedTradableManager; import io.bitsquare.trade.closed.ClosedTradableManager;
import io.bitsquare.trade.failed.FailedTradesManager; import io.bitsquare.trade.failed.FailedTradesManager;
import io.bitsquare.user.Preferences; import io.bitsquare.user.Preferences;
import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
@ -50,10 +51,7 @@ import javafx.scene.control.*;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.util.Callback; import javafx.util.Callback;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.*;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.Transaction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.KeyParameter;
@ -87,7 +85,10 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private Set<WithdrawalListItem> selectedItems = new HashSet<>(); private Set<WithdrawalListItem> selectedItems = new HashSet<>();
private BalanceListener balanceListener; private BalanceListener balanceListener;
private Set<String> fromAddresses; private Set<String> fromAddresses;
private Coin amountOfSelectedItems; private Coin amountOfSelectedItems = Coin.ZERO;
private ObjectProperty<Coin> senderAmountAsCoinProperty = new SimpleObjectProperty<>(Coin.ZERO);
private ChangeListener<String> amountListener;
private ChangeListener<Boolean> amountFocusListener;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -131,6 +132,23 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
updateList(); updateList();
} }
}; };
amountListener = (observable, oldValue, newValue) -> {
if (amountTextField.focusedProperty().get()) {
try {
senderAmountAsCoinProperty.set(formatter.parseToCoin(amountTextField.getText()));
} catch (Throwable t) {
log.error("Error at amountTextField input. " + t.toString());
}
}
};
amountFocusListener = (observable, oldValue, newValue) -> {
if (oldValue && !newValue) {
if (senderAmountAsCoinProperty.get().isPositive())
amountTextField.setText(formatter.formatCoin(senderAmountAsCoinProperty.get()));
else
amountTextField.setText("");
}
};
} }
@Override @Override
@ -141,17 +159,18 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
reset(); reset();
amountTextField.textProperty().addListener(amountListener);
amountTextField.focusedProperty().addListener(amountFocusListener);
walletService.addBalanceListener(balanceListener); walletService.addBalanceListener(balanceListener);
withdrawButton.disableProperty().bind(Bindings.createBooleanBinding(() -> !areInputsValid(),
amountTextField.textProperty(), withdrawToTextField.textProperty()));
} }
@Override @Override
protected void deactivate() { protected void deactivate() {
sortedList.comparatorProperty().unbind(); sortedList.comparatorProperty().unbind();
observableList.forEach(WithdrawalListItem::cleanup); observableList.forEach(WithdrawalListItem::cleanup);
withdrawButton.disableProperty().unbind();
walletService.removeBalanceListener(balanceListener); walletService.removeBalanceListener(balanceListener);
amountTextField.textProperty().removeListener(amountListener);
amountTextField.focusedProperty().removeListener(amountFocusListener);
} }
@ -161,8 +180,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
@FXML @FXML
public void onWithdraw() { public void onWithdraw() {
Coin senderAmount = formatter.parseToCoin(amountTextField.getText()); if (areInputsValid()) {
if (Restrictions.isAboveFixedTxFeeAndDust(senderAmount)) {
FutureCallback<Transaction> callback = new FutureCallback<Transaction>() { FutureCallback<Transaction> callback = new FutureCallback<Transaction>() {
@Override @Override
public void onSuccess(@javax.annotation.Nullable Transaction transaction) { public void onSuccess(@javax.annotation.Nullable Transaction transaction) {
@ -192,12 +210,13 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
// TODO Get a proper fee calculation from BitcoinJ directly // TODO Get a proper fee calculation from BitcoinJ directly
Coin requiredFee = walletService.getRequiredFeeForMultipleAddresses(fromAddresses, Coin requiredFee = walletService.getRequiredFeeForMultipleAddresses(fromAddresses,
withdrawToTextField.getText(), amountOfSelectedItems); withdrawToTextField.getText(), amountOfSelectedItems);
Coin receiverAmount = senderAmount.subtract(requiredFee); Coin receiverAmount = senderAmountAsCoinProperty.get().subtract(requiredFee);
if (receiverAmount.isPositive()) {
if (BitsquareApp.DEV_MODE) { if (BitsquareApp.DEV_MODE) {
doWithdraw(receiverAmount, callback); doWithdraw(receiverAmount, callback);
} else { } else {
new Popup().headLine("Confirm withdrawal request") new Popup().headLine("Confirm withdrawal request")
.confirmation("Sending: " + formatter.formatCoinWithCode(senderAmount) + "\n" + .confirmation("Sending: " + formatter.formatCoinWithCode(senderAmountAsCoinProperty.get()) + "\n" +
"From address: " + withdrawFromTextField.getText() + "\n" + "From address: " + withdrawFromTextField.getText() + "\n" +
"To receiving address: " + withdrawToTextField.getText() + ".\n" + "To receiving address: " + withdrawToTextField.getText() + ".\n" +
"Required transaction fee is: " + formatter.formatCoinWithCode(requiredFee) + "\n\n" + "Required transaction fee is: " + formatter.formatCoinWithCode(requiredFee) + "\n\n" +
@ -209,14 +228,15 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
.show(); .show();
} }
} else {
new Popup().warning("The amount you would like to send is too low as the bitcoin transaction fee will be deducted.\n" +
"Please use a higher amount.").show();
}
} catch (Throwable e) { } catch (Throwable e) {
e.printStackTrace(); e.printStackTrace();
log.error(e.getMessage()); log.error(e.getMessage());
new Popup().error(e.getMessage()).show(); new Popup().warning(e.getMessage()).show();
} }
} else {
new Popup().warning("The amount to transfer is lower than the transaction fee and the min. possible tx value (dust).")
.show();
} }
} }
@ -233,8 +253,11 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
if (!selectedItems.isEmpty()) { if (!selectedItems.isEmpty()) {
amountOfSelectedItems = Coin.valueOf(selectedItems.stream().mapToLong(e -> e.getBalance().getValue()).sum()); amountOfSelectedItems = Coin.valueOf(selectedItems.stream().mapToLong(e -> e.getBalance().getValue()).sum());
if (amountOfSelectedItems.isPositive()) { if (amountOfSelectedItems.isPositive()) {
senderAmountAsCoinProperty.set(amountOfSelectedItems);
amountTextField.setText(formatter.formatCoin(amountOfSelectedItems)); amountTextField.setText(formatter.formatCoin(amountOfSelectedItems));
} else { } else {
senderAmountAsCoinProperty.set(Coin.ZERO);
amountOfSelectedItems = Coin.ZERO;
amountTextField.setText(""); amountTextField.setText("");
withdrawFromTextField.setText(""); withdrawFromTextField.setText("");
} }
@ -302,11 +325,17 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
updateList(); updateList();
} catch (AddressFormatException e) { } catch (AddressFormatException e) {
new Popup().warning("The address is not correct. Please check the address format.").show(); new Popup().warning("The address is not correct. Please check the address format.").show();
} catch (Wallet.DustySendRequested e) {
new Popup().warning("The amount you would like to send is below the dust limit and would be rejected by the bitcoin network.\n" +
"Please use a higher amount.").show();
} catch (AddressEntryException e) { } catch (AddressEntryException e) {
new Popup().error(e.getMessage()).show(); new Popup().error(e.getMessage()).show();
} catch (InsufficientMoneyException e) { } catch (InsufficientMoneyException e) {
log.warn(e.getMessage()); log.warn(e.getMessage());
new Popup().warning("You don't have enough fund in your wallet.").show(); new Popup().warning("You don't have enough fund in your wallet.").show();
} catch (Throwable e) {
log.warn(e.getMessage());
new Popup().warning(e.getMessage()).show();
} }
} }
@ -319,6 +348,8 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
withdrawFromTextField.setPromptText("Select a source address from the table"); withdrawFromTextField.setPromptText("Select a source address from the table");
withdrawFromTextField.setTooltip(null); withdrawFromTextField.setTooltip(null);
amountOfSelectedItems = Coin.ZERO;
senderAmountAsCoinProperty.set(Coin.ZERO);
amountTextField.setText(""); amountTextField.setText("");
amountTextField.setPromptText("Set the amount to withdraw"); amountTextField.setPromptText("Set the amount to withdraw");
@ -342,14 +373,27 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
} }
private boolean areInputsValid() { private boolean areInputsValid() {
if (amountTextField.getText().length() > 0) { if (!senderAmountAsCoinProperty.get().isPositive()) {
Coin amount = formatter.parseToCoin(amountTextField.getText()); new Popup().warning("Please fill in a valid value for the amount to send (max. 8 decimal places).").show();
return btcAddressValidator.validate(withdrawToTextField.getText()).isValid &&
amount.compareTo(amountOfSelectedItems) <= 0 &&
Restrictions.isAboveFixedTxFeeAndDust(amount);
} else {
return false; return false;
} }
if (!btcAddressValidator.validate(withdrawToTextField.getText()).isValid) {
new Popup().warning("Please fill in a valid receiver bitcoin address.").show();
return false;
}
if (!amountOfSelectedItems.isPositive()) {
new Popup().warning("You need to select a source address in the table above.").show();
return false;
}
if (senderAmountAsCoinProperty.get().compareTo(amountOfSelectedItems) > 0) {
new Popup().warning("Your amount exceeds the available amount for the selected address.\n" +
"Consider to select multiple addresses in the table above if you want to withdraw more.").show();
return false;
}
return true;
} }

View file

@ -183,7 +183,7 @@ public class BuyerStep5View extends TradeStepView {
} else { } else {
if (toAddresses.isEmpty()) { if (toAddresses.isEmpty()) {
validateWithdrawAddress(); validateWithdrawAddress();
} else if (Restrictions.isAboveFixedTxFeeAndDust(senderAmount)) { } else if (Restrictions.isAboveFixedTxFeeForTradesAndDust(senderAmount)) {
if (BitsquareApp.DEV_MODE) { if (BitsquareApp.DEV_MODE) {
doWithdrawal(receiverAmount); doWithdrawal(receiverAmount);

View file

@ -53,7 +53,7 @@ public class BSFormatter {
// Input of a group separator (1,123,45) lead to an validation error. // Input of a group separator (1,123,45) lead to an validation error.
// Note: BtcFormat was intended to be used, but it lead to many problems (automatic format to mBit, // Note: BtcFormat was intended to be used, but it lead to many problems (automatic format to mBit,
// no way to remove grouping separator). It seems to be not optimal for user input formatting. // no way to remove grouping separator). It seems to be not optimal for user input formatting.
private MonetaryFormat coinFormat = MonetaryFormat.BTC.repeatOptionalDecimals(2, 2); private MonetaryFormat coinFormat = MonetaryFormat.BTC.minDecimals(2).repeatOptionalDecimals(1, 6);
// private String currencyCode = CurrencyUtil.getDefaultFiatCurrencyAsCode(); // private String currencyCode = CurrencyUtil.getDefaultFiatCurrencyAsCode();
@ -97,7 +97,7 @@ public class BSFormatter {
if (useMilliBit) if (useMilliBit)
return MonetaryFormat.MBTC; return MonetaryFormat.MBTC;
else else
return MonetaryFormat.BTC.repeatOptionalDecimals(2, 2); return MonetaryFormat.BTC.minDecimals(2).repeatOptionalDecimals(1, 6);
} }
/* public void setFiatCurrencyCode(String currencyCode) { /* public void setFiatCurrencyCode(String currencyCode) {