replace miner fee toggles with 'send max' link for withdraw #1033

Co-authored-by: sraver <pyrdnx@pm.me>
Co-authored-by: woodser <woodser@protonmail.com>
This commit is contained in:
sraver 2024-06-30 19:36:06 +08:00 committed by GitHub
parent 7acba27b9d
commit c3b93b6e75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 50 additions and 54 deletions

View File

@ -1040,11 +1040,13 @@ funds.withdrawal.inputs=Inputs selection
funds.withdrawal.useAllInputs=Use all available inputs funds.withdrawal.useAllInputs=Use all available inputs
funds.withdrawal.useCustomInputs=Use custom inputs funds.withdrawal.useCustomInputs=Use custom inputs
funds.withdrawal.receiverAmount=Receiver's amount funds.withdrawal.receiverAmount=Receiver's amount
funds.withdrawal.sendMax=Send max available
funds.withdrawal.senderAmount=Sender's amount funds.withdrawal.senderAmount=Sender's amount
funds.withdrawal.feeExcluded=Amount excludes mining fee funds.withdrawal.feeExcluded=Amount excludes mining fee
funds.withdrawal.feeIncluded=Amount includes mining fee funds.withdrawal.feeIncluded=Amount includes mining fee
funds.withdrawal.fromLabel=Withdraw from address funds.withdrawal.fromLabel=Withdraw from address
funds.withdrawal.toLabel=Withdraw to address funds.withdrawal.toLabel=Withdraw to address
funds.withdrawal.maximum=MAX
funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memoLabel=Withdrawal memo
funds.withdrawal.memo=Optionally fill memo funds.withdrawal.memo=Optionally fill memo
funds.withdrawal.withdrawButton=Withdraw selected funds.withdrawal.withdrawButton=Withdraw selected

View File

@ -848,6 +848,7 @@ funds.withdrawal.inputs=Selección de entradas
funds.withdrawal.useAllInputs=Usar todos los entradas disponibles funds.withdrawal.useAllInputs=Usar todos los entradas disponibles
funds.withdrawal.useCustomInputs=Usar entradas personalizados funds.withdrawal.useCustomInputs=Usar entradas personalizados
funds.withdrawal.receiverAmount=Cantidad del receptor funds.withdrawal.receiverAmount=Cantidad del receptor
funds.withdrawal.sendMax=Enviar máximo disponible
funds.withdrawal.senderAmount=Cantidad del emisor funds.withdrawal.senderAmount=Cantidad del emisor
funds.withdrawal.feeExcluded=La cantidad no incluye comisión de minado funds.withdrawal.feeExcluded=La cantidad no incluye comisión de minado
funds.withdrawal.feeIncluded=La cantidad incluye comisión de minado funds.withdrawal.feeIncluded=La cantidad incluye comisión de minado

View File

@ -849,6 +849,7 @@ funds.withdrawal.inputs=Sélection de la valeur à saisir
funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles
funds.withdrawal.useCustomInputs=Utiliser une valeur de saisie personnalisée funds.withdrawal.useCustomInputs=Utiliser une valeur de saisie personnalisée
funds.withdrawal.receiverAmount=Montant du destinataire funds.withdrawal.receiverAmount=Montant du destinataire
funds.withdrawal.sendMax=Envoyer max disponible
funds.withdrawal.senderAmount=Montant de l'expéditeur funds.withdrawal.senderAmount=Montant de l'expéditeur
funds.withdrawal.feeExcluded=Montant excluant les frais de minage funds.withdrawal.feeExcluded=Montant excluant les frais de minage
funds.withdrawal.feeIncluded=Montant incluant frais de minage funds.withdrawal.feeIncluded=Montant incluant frais de minage

View File

@ -847,6 +847,7 @@ funds.withdrawal.inputs=Selezione input
funds.withdrawal.useAllInputs=Utilizza tutti gli input disponibili funds.withdrawal.useAllInputs=Utilizza tutti gli input disponibili
funds.withdrawal.useCustomInputs=Utilizza input personalizzati funds.withdrawal.useCustomInputs=Utilizza input personalizzati
funds.withdrawal.receiverAmount=Importo del destinatario funds.withdrawal.receiverAmount=Importo del destinatario
funds.withdrawal.sendMax=Inviare massimo disponibile
funds.withdrawal.senderAmount=Importo del mittente funds.withdrawal.senderAmount=Importo del mittente
funds.withdrawal.feeExcluded=L'importo esclude la commissione di mining funds.withdrawal.feeExcluded=L'importo esclude la commissione di mining
funds.withdrawal.feeIncluded=L'importo include la commissione di mining funds.withdrawal.feeIncluded=L'importo include la commissione di mining

View File

@ -35,7 +35,7 @@
package haveno.desktop.main.funds.withdrawal; package haveno.desktop.main.funds.withdrawal;
import com.google.inject.Inject; import com.google.inject.Inject;
import haveno.common.util.Tuple4; import haveno.common.util.Tuple3;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.TradeManager; 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.ActivatableView;
import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.FxmlView;
import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.BusyAnimation;
import haveno.desktop.components.HyperlinkWithIcon;
import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.TitledGroupBg;
import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.main.overlays.windows.TxDetails; import haveno.desktop.main.overlays.windows.TxDetails;
@ -60,13 +61,11 @@ import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TextField; 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.control.Button;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import monero.common.MoneroUtils;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -90,7 +89,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private Label amountLabel; private Label amountLabel;
private TextField amountTextField, withdrawToTextField, withdrawMemoTextField; private TextField amountTextField, withdrawToTextField, withdrawMemoTextField;
private RadioButton feeExcludedRadioButton, feeIncludedRadioButton;
private final XmrWalletService xmrWalletService; private final XmrWalletService xmrWalletService;
private final TradeManager tradeManager; private final TradeManager tradeManager;
@ -100,11 +98,9 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private BigInteger amount = BigInteger.ZERO; private BigInteger amount = BigInteger.ZERO;
private ChangeListener<String> amountListener; private ChangeListener<String> amountListener;
private ChangeListener<Boolean> amountFocusListener; private ChangeListener<Boolean> amountFocusListener;
private ChangeListener<Toggle> feeToggleGroupListener;
private ToggleGroup feeToggleGroup;
private boolean feeExcluded;
private int rowIndex = 0; private int rowIndex = 0;
private final static int MAX_ATTEMPTS = 3; private final static int MAX_ATTEMPTS = 3;
boolean sendMax = false;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle // Constructor, lifecycle
@ -141,20 +137,15 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
withdrawToTextField = addTopLabelInputTextField(gridPane, ++rowIndex, withdrawToTextField = addTopLabelInputTextField(gridPane, ++rowIndex,
Res.get("funds.withdrawal.toLabel", Res.getBaseCurrencyCode())).second; Res.get("funds.withdrawal.toLabel", Res.getBaseCurrencyCode())).second;
feeToggleGroup = new ToggleGroup(); final Tuple3<Label, TextField, HyperlinkWithIcon> feeTuple3 = FormBuilder.addTopLabelTextFieldHyperLink(gridPane, ++rowIndex, "",
final Tuple4<Label, TextField, RadioButton, RadioButton> feeTuple3 = FormBuilder.addTopLabelTextFieldRadioButtonRadioButton(gridPane, ++rowIndex, feeToggleGroup,
Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()), Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()),
"", Res.get("funds.withdrawal.sendMax"),
Res.get("funds.withdrawal.feeExcluded"),
Res.get("funds.withdrawal.feeIncluded"),
0); 0);
amountLabel = feeTuple3.first; amountLabel = feeTuple3.first;
amountTextField = feeTuple3.second; amountTextField = feeTuple3.second;
amountTextField.setMinWidth(180); amountTextField.setMinWidth(180);
feeExcludedRadioButton = feeTuple3.third; HyperlinkWithIcon sendMaxLink = feeTuple3.third;
feeIncludedRadioButton = feeTuple3.fourth;
withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex,
Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second; Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second;
@ -175,6 +166,12 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
}).start(); }).start();
}); });
sendMaxLink.setOnAction(event -> {
sendMax = true;
amount = null; // set amount when tx created
amountTextField.setText(Res.get("funds.withdrawal.maximum"));
});
balanceListener = new XmrBalanceListener() { balanceListener = new XmrBalanceListener() {
@Override @Override
public void onBalanceChanged(BigInteger balance) { public void onBalanceChanged(BigInteger balance) {
@ -183,6 +180,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
}; };
amountListener = (observable, oldValue, newValue) -> { amountListener = (observable, oldValue, newValue) -> {
if (amountTextField.focusedProperty().get()) { if (amountTextField.focusedProperty().get()) {
sendMax = false; // disable max if amount changed while focused
try { try {
amount = HavenoUtils.parseXmr(amountTextField.getText()); amount = HavenoUtils.parseXmr(amountTextField.getText());
} catch (Throwable t) { } catch (Throwable t) {
@ -191,7 +189,9 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
} }
}; };
amountFocusListener = (observable, oldValue, newValue) -> { 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) if (amount.compareTo(BigInteger.ZERO) > 0)
amountTextField.setText(HavenoUtils.formatXmr(amount)); amountTextField.setText(HavenoUtils.formatXmr(amount));
else else
@ -199,14 +199,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
} }
}; };
amountLabel.setText(Res.get("funds.withdrawal.receiverAmount")); 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<VBox, Void> {
amountTextField.textProperty().addListener(amountListener); amountTextField.textProperty().addListener(amountListener);
amountTextField.focusedProperty().addListener(amountFocusListener); amountTextField.focusedProperty().addListener(amountFocusListener);
xmrWalletService.addBalanceListener(balanceListener); xmrWalletService.addBalanceListener(balanceListener);
feeToggleGroup.selectedToggleProperty().addListener(feeToggleGroupListener);
if (feeToggleGroup.getSelectedToggle() == null) feeToggleGroup.selectToggle(feeExcludedRadioButton);
GUIUtil.requestFocus(withdrawToTextField); GUIUtil.requestFocus(withdrawToTextField);
} }
@ -242,7 +231,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
xmrWalletService.removeBalanceListener(balanceListener); xmrWalletService.removeBalanceListener(balanceListener);
amountTextField.textProperty().removeListener(amountListener); amountTextField.textProperty().removeListener(amountListener);
amountTextField.focusedProperty().removeListener(amountFocusListener); amountTextField.focusedProperty().removeListener(amountFocusListener);
feeToggleGroup.selectedToggleProperty().removeListener(feeToggleGroupListener);
} }
@ -254,8 +242,14 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(xmrWalletService)) { if (GUIUtil.isReadyForTxBroadcastOrShowPopup(xmrWalletService)) {
try { try {
// get withdraw address // validate address
final String withdrawToAddress = withdrawToTextField.getText(); 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 // check sufficient available balance
if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow")); 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<VBox, Void> {
.setAccountIndex(0) .setAccountIndex(0)
.setAmount(amount) .setAmount(amount)
.setAddress(withdrawToAddress) .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); log.info("Done creating withdraw tx in {} ms", System.currentTimeMillis() - startTime);
break; break;
} catch (Exception e) { } catch (Exception e) {
if (isNotEnoughMoney(e.getMessage())) throw e;
log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, MAX_ATTEMPTS, e.getMessage()); log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, MAX_ATTEMPTS, e.getMessage());
if (i == MAX_ATTEMPTS - 1) throw e; if (i == MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
@ -283,15 +278,17 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
// popup confirmation message // popup confirmation message
popupConfirmationMessage(tx); popupConfirmationMessage(tx);
} catch (Throwable e) { } catch (Throwable e) {
if (e.getMessage().contains("enough")) new Popup().warning(Res.get("funds.withdrawal.warn.amountExceeds")).show(); e.printStackTrace();
else { if (isNotEnoughMoney(e.getMessage())) new Popup().warning(Res.get("funds.withdrawal.notEnoughFunds")).show();
e.printStackTrace(); else new Popup().warning(e.getMessage()).show();
new Popup().warning(e.getMessage()).show();
}
} }
} }
} }
private static boolean isNotEnoughMoney(String errorMsg) {
return errorMsg.contains("not enough");
}
private void popupConfirmationMessage(MoneroTxWallet tx) { private void popupConfirmationMessage(MoneroTxWallet tx) {
// create confirmation message // create confirmation message
@ -347,6 +344,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void reset() { private void reset() {
sendMax = false;
amount = BigInteger.ZERO; amount = BigInteger.ZERO;
amountTextField.setText(""); amountTextField.setText("");
amountTextField.setPromptText(Res.get("funds.withdrawal.setAmount")); amountTextField.setPromptText(Res.get("funds.withdrawal.setAmount"));

View File

@ -1159,35 +1159,28 @@ public class FormBuilder {
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Label + TextField + RadioButton + RadioButton // Label + TextField + HyperlinkWithIcon
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public static Tuple4<Label, TextField, RadioButton, RadioButton> addTopLabelTextFieldRadioButtonRadioButton(GridPane gridPane, public static Tuple3<Label, TextField, HyperlinkWithIcon> addTopLabelTextFieldHyperLink(GridPane gridPane,
int rowIndex, int rowIndex,
ToggleGroup toggleGroup, String title,
String title, String textFieldTitle,
String textFieldTitle, String maxButtonTitle,
String radioButtonTitle1, double top) {
String radioButtonTitle2,
double top) {
TextField textField = new HavenoTextField(); TextField textField = new HavenoTextField();
textField.setPromptText(textFieldTitle); textField.setPromptText(textFieldTitle);
RadioButton radioButton1 = new AutoTooltipRadioButton(radioButtonTitle1); HyperlinkWithIcon maxLink = new ExternalHyperlink(maxButtonTitle);
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));
HBox hBox = new HBox(); HBox hBox = new HBox();
hBox.setSpacing(10); hBox.setSpacing(10);
hBox.getChildren().addAll(textField, radioButton1, radioButton2); hBox.getChildren().addAll(textField, maxLink);
hBox.setAlignment(Pos.CENTER_LEFT);
final Tuple2<Label, VBox> labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); final Tuple2<Label, VBox> labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top);
return new Tuple4<>(labelVBoxTuple2.first, textField, radioButton1, radioButton2); return new Tuple3<>(labelVBoxTuple2.first, textField, maxLink);
} }