diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index e0c28fb8dd..f8ef28949c 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1040,11 +1040,13 @@ funds.withdrawal.inputs=Inputs selection funds.withdrawal.useAllInputs=Use all available inputs funds.withdrawal.useCustomInputs=Use custom inputs funds.withdrawal.receiverAmount=Receiver's amount +funds.withdrawal.sendMax=Send max available funds.withdrawal.senderAmount=Sender's amount funds.withdrawal.feeExcluded=Amount excludes mining fee funds.withdrawal.feeIncluded=Amount includes mining fee funds.withdrawal.fromLabel=Withdraw from address funds.withdrawal.toLabel=Withdraw to address +funds.withdrawal.maximum=MAX funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=Withdraw selected diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 990ef7941c..bd4ee417a2 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -848,6 +848,7 @@ funds.withdrawal.inputs=Selección de entradas funds.withdrawal.useAllInputs=Usar todos los entradas disponibles funds.withdrawal.useCustomInputs=Usar entradas personalizados funds.withdrawal.receiverAmount=Cantidad del receptor +funds.withdrawal.sendMax=Enviar máximo disponible funds.withdrawal.senderAmount=Cantidad del emisor funds.withdrawal.feeExcluded=La cantidad no incluye comisión de minado funds.withdrawal.feeIncluded=La cantidad incluye comisión de minado diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 462bcef554..8d3b4ce468 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -849,6 +849,7 @@ funds.withdrawal.inputs=Sélection de la valeur à saisir funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles funds.withdrawal.useCustomInputs=Utiliser une valeur de saisie personnalisée funds.withdrawal.receiverAmount=Montant du destinataire +funds.withdrawal.sendMax=Envoyer max disponible funds.withdrawal.senderAmount=Montant de l'expéditeur funds.withdrawal.feeExcluded=Montant excluant les frais de minage funds.withdrawal.feeIncluded=Montant incluant frais de minage diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 97b7fb6b15..044873db1a 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -847,6 +847,7 @@ funds.withdrawal.inputs=Selezione input funds.withdrawal.useAllInputs=Utilizza tutti gli input disponibili funds.withdrawal.useCustomInputs=Utilizza input personalizzati funds.withdrawal.receiverAmount=Importo del destinatario +funds.withdrawal.sendMax=Inviare massimo disponibile funds.withdrawal.senderAmount=Importo del mittente funds.withdrawal.feeExcluded=L'importo esclude la commissione di mining funds.withdrawal.feeIncluded=L'importo include la commissione di mining diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index abdcde0b1b..c5e40d9348 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -35,7 +35,7 @@ package haveno.desktop.main.funds.withdrawal; import com.google.inject.Inject; -import haveno.common.util.Tuple4; +import haveno.common.util.Tuple3; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; @@ -48,6 +48,7 @@ import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.BusyAnimation; +import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TxDetails; @@ -60,13 +61,11 @@ import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.TextField; -import javafx.scene.control.RadioButton; -import javafx.scene.control.ToggleGroup; -import javafx.scene.control.Toggle; import javafx.scene.control.Button; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import monero.common.MoneroUtils; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxWallet; @@ -90,7 +89,6 @@ public class WithdrawalView extends ActivatableView { private Label amountLabel; private TextField amountTextField, withdrawToTextField, withdrawMemoTextField; - private RadioButton feeExcludedRadioButton, feeIncludedRadioButton; private final XmrWalletService xmrWalletService; private final TradeManager tradeManager; @@ -100,11 +98,9 @@ public class WithdrawalView extends ActivatableView { private BigInteger amount = BigInteger.ZERO; private ChangeListener amountListener; private ChangeListener amountFocusListener; - private ChangeListener feeToggleGroupListener; - private ToggleGroup feeToggleGroup; - private boolean feeExcluded; private int rowIndex = 0; private final static int MAX_ATTEMPTS = 3; + boolean sendMax = false; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -141,20 +137,15 @@ public class WithdrawalView extends ActivatableView { withdrawToTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.toLabel", Res.getBaseCurrencyCode())).second; - feeToggleGroup = new ToggleGroup(); - - final Tuple4 feeTuple3 = FormBuilder.addTopLabelTextFieldRadioButtonRadioButton(gridPane, ++rowIndex, feeToggleGroup, + final Tuple3 feeTuple3 = FormBuilder.addTopLabelTextFieldHyperLink(gridPane, ++rowIndex, "", Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()), - "", - Res.get("funds.withdrawal.feeExcluded"), - Res.get("funds.withdrawal.feeIncluded"), + Res.get("funds.withdrawal.sendMax"), 0); amountLabel = feeTuple3.first; amountTextField = feeTuple3.second; amountTextField.setMinWidth(180); - feeExcludedRadioButton = feeTuple3.third; - feeIncludedRadioButton = feeTuple3.fourth; + HyperlinkWithIcon sendMaxLink = feeTuple3.third; withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second; @@ -175,6 +166,12 @@ public class WithdrawalView extends ActivatableView { }).start(); }); + sendMaxLink.setOnAction(event -> { + sendMax = true; + amount = null; // set amount when tx created + amountTextField.setText(Res.get("funds.withdrawal.maximum")); + }); + balanceListener = new XmrBalanceListener() { @Override public void onBalanceChanged(BigInteger balance) { @@ -183,6 +180,7 @@ public class WithdrawalView extends ActivatableView { }; amountListener = (observable, oldValue, newValue) -> { if (amountTextField.focusedProperty().get()) { + sendMax = false; // disable max if amount changed while focused try { amount = HavenoUtils.parseXmr(amountTextField.getText()); } catch (Throwable t) { @@ -191,7 +189,9 @@ public class WithdrawalView extends ActivatableView { } }; amountFocusListener = (observable, oldValue, newValue) -> { - if (oldValue && !newValue) { + + // parse amount on focus out unless sending max + if (oldValue && !newValue && !sendMax) { if (amount.compareTo(BigInteger.ZERO) > 0) amountTextField.setText(HavenoUtils.formatXmr(amount)); else @@ -199,14 +199,6 @@ public class WithdrawalView extends ActivatableView { } }; amountLabel.setText(Res.get("funds.withdrawal.receiverAmount")); - feeExcludedRadioButton.setToggleGroup(feeToggleGroup); - feeIncludedRadioButton.setToggleGroup(feeToggleGroup); - feeToggleGroupListener = (observable, oldValue, newValue) -> { - feeExcluded = newValue == feeExcludedRadioButton; - amountLabel.setText(feeExcluded ? - Res.get("funds.withdrawal.receiverAmount") : - Res.get("funds.withdrawal.senderAmount")); - }; } @@ -229,9 +221,6 @@ public class WithdrawalView extends ActivatableView { amountTextField.textProperty().addListener(amountListener); amountTextField.focusedProperty().addListener(amountFocusListener); xmrWalletService.addBalanceListener(balanceListener); - feeToggleGroup.selectedToggleProperty().addListener(feeToggleGroupListener); - - if (feeToggleGroup.getSelectedToggle() == null) feeToggleGroup.selectToggle(feeExcludedRadioButton); GUIUtil.requestFocus(withdrawToTextField); } @@ -242,7 +231,6 @@ public class WithdrawalView extends ActivatableView { xmrWalletService.removeBalanceListener(balanceListener); amountTextField.textProperty().removeListener(amountListener); amountTextField.focusedProperty().removeListener(amountFocusListener); - feeToggleGroup.selectedToggleProperty().removeListener(feeToggleGroupListener); } @@ -254,8 +242,14 @@ public class WithdrawalView extends ActivatableView { if (GUIUtil.isReadyForTxBroadcastOrShowPopup(xmrWalletService)) { try { - // get withdraw address + // validate address final String withdrawToAddress = withdrawToTextField.getText(); + if (!MoneroUtils.isValidAddress(withdrawToAddress, XmrWalletService.getMoneroNetworkType())) { + throw new IllegalArgumentException(Res.get("validation.xmr.invalidAddress")); + } + + // set max amount if requested + if (sendMax) amount = xmrWalletService.getAvailableBalance(); // check sufficient available balance if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow")); @@ -270,10 +264,11 @@ public class WithdrawalView extends ActivatableView { .setAccountIndex(0) .setAmount(amount) .setAddress(withdrawToAddress) - .setSubtractFeeFrom(feeExcluded ? null : Arrays.asList(0))); + .setSubtractFeeFrom(sendMax ? Arrays.asList(0) : null)); log.info("Done creating withdraw tx in {} ms", System.currentTimeMillis() - startTime); break; } catch (Exception e) { + if (isNotEnoughMoney(e.getMessage())) throw e; log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, MAX_ATTEMPTS, e.getMessage()); if (i == MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying @@ -283,15 +278,17 @@ public class WithdrawalView extends ActivatableView { // popup confirmation message popupConfirmationMessage(tx); } catch (Throwable e) { - if (e.getMessage().contains("enough")) new Popup().warning(Res.get("funds.withdrawal.warn.amountExceeds")).show(); - else { - e.printStackTrace(); - new Popup().warning(e.getMessage()).show(); - } + e.printStackTrace(); + if (isNotEnoughMoney(e.getMessage())) new Popup().warning(Res.get("funds.withdrawal.notEnoughFunds")).show(); + else new Popup().warning(e.getMessage()).show(); } } } + private static boolean isNotEnoughMoney(String errorMsg) { + return errorMsg.contains("not enough"); + } + private void popupConfirmationMessage(MoneroTxWallet tx) { // create confirmation message @@ -347,6 +344,7 @@ public class WithdrawalView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void reset() { + sendMax = false; amount = BigInteger.ZERO; amountTextField.setText(""); amountTextField.setPromptText(Res.get("funds.withdrawal.setAmount")); diff --git a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java index ba698752a4..cae26b805a 100644 --- a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java @@ -1159,35 +1159,28 @@ public class FormBuilder { } /////////////////////////////////////////////////////////////////////////////////////////// - // Label + TextField + RadioButton + RadioButton + // Label + TextField + HyperlinkWithIcon /////////////////////////////////////////////////////////////////////////////////////////// - public static Tuple4 addTopLabelTextFieldRadioButtonRadioButton(GridPane gridPane, - int rowIndex, - ToggleGroup toggleGroup, - String title, - String textFieldTitle, - String radioButtonTitle1, - String radioButtonTitle2, - double top) { + public static Tuple3 addTopLabelTextFieldHyperLink(GridPane gridPane, + int rowIndex, + String title, + String textFieldTitle, + String maxButtonTitle, + double top) { TextField textField = new HavenoTextField(); textField.setPromptText(textFieldTitle); - RadioButton radioButton1 = new AutoTooltipRadioButton(radioButtonTitle1); - radioButton1.setToggleGroup(toggleGroup); - radioButton1.setPadding(new Insets(6, 0, 0, 0)); - - RadioButton radioButton2 = new AutoTooltipRadioButton(radioButtonTitle2); - radioButton2.setToggleGroup(toggleGroup); - radioButton2.setPadding(new Insets(6, 0, 0, 0)); + HyperlinkWithIcon maxLink = new ExternalHyperlink(maxButtonTitle); HBox hBox = new HBox(); hBox.setSpacing(10); - hBox.getChildren().addAll(textField, radioButton1, radioButton2); + hBox.getChildren().addAll(textField, maxLink); + hBox.setAlignment(Pos.CENTER_LEFT); final Tuple2 labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); - return new Tuple4<>(labelVBoxTuple2.first, textField, radioButton1, radioButton2); + return new Tuple3<>(labelVBoxTuple2.first, textField, maxLink); }