From c3b93b6e756e97664566e0a6fad911910776fc3a Mon Sep 17 00:00:00 2001 From: sraver Date: Sun, 30 Jun 2024 19:36:06 +0800 Subject: [PATCH 01/58] replace miner fee toggles with 'send max' link for withdraw #1033 Co-authored-by: sraver Co-authored-by: woodser --- .../resources/i18n/displayStrings.properties | 2 + .../i18n/displayStrings_es.properties | 1 + .../i18n/displayStrings_fr.properties | 1 + .../i18n/displayStrings_it.properties | 1 + .../main/funds/withdrawal/WithdrawalView.java | 70 +++++++++---------- .../java/haveno/desktop/util/FormBuilder.java | 29 +++----- 6 files changed, 50 insertions(+), 54 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index e0c28fb8..f8ef2894 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 990ef794..bd4ee417 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 462bcef5..8d3b4ce4 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 97b7fb6b..044873db 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 abdcde0b..c5e40d93 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 ba698752..cae26b80 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); } From 1debdde33eb789eeda0068bd350ac1a878f22914 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 30 Jun 2024 07:36:25 -0400 Subject: [PATCH 02/58] skip wallet polls if daemon not synced (#1080) --- core/src/main/java/haveno/core/trade/Trade.java | 4 +++- .../java/haveno/core/xmr/wallet/XmrWalletService.java | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index da7dbb85..6aa00373 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2404,8 +2404,10 @@ public abstract class Trade implements Tradable, Model { // skip if deposit txs unknown or not requested if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; + // skip if daemon not synced + if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return; + // sync if wallet too far behind daemon - if (xmrConnectionService.getTargetHeight() == null) return; if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false); // update deposit txs diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 21b5d14d..682aafdb 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1803,12 +1803,18 @@ public class XmrWalletService { pollInProgress = true; try { - // switch to best connection if daemon is too far behind + // skip if daemon not synced MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); if (lastInfo == null) { log.warn("Last daemon info is null"); return; } + if (!xmrConnectionService.isSyncedWithinTolerance()) { + log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight()); + return; + } + + // switch to best connection if wallet is too far behind if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) { log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight()); xmrConnectionService.switchToBestConnection(); From 856faafd1ce45aead9648d000d133048298f216e Mon Sep 17 00:00:00 2001 From: fa2a5qj3 <174058787+fa2a5qj3@users.noreply.github.com> Date: Sun, 30 Jun 2024 08:56:39 -0400 Subject: [PATCH 03/58] fix IPv6 connectivity to XMR nodes #1076 --- core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java index 362211d1..894a71ce 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -150,7 +150,7 @@ public class XmrNodes { if (parts[0].contains("[") && parts[0].contains(":")) { // IPv6 address and optional port number // address part delimited by square brackets e.g. [2a01:123:456:789::2]:8333 - host = parts[0].replace("[", "").replace("]", ""); + host = parts[0] + "]"; // keep the square brackets per RFC-2732 if (parts.length == 2) port = Integer.parseInt(parts[1].replace(":", "")); } else if (parts[0].contains(":") && !parts[0].contains(".")) { From c01d63490f55cfd6f3eaed4fc029d728b794533d Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 1 Jul 2024 08:41:17 -0400 Subject: [PATCH 04/58] fix null price feed listener --- .../core/provider/price/PriceFeedService.java | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/haveno/core/provider/price/PriceFeedService.java b/core/src/main/java/haveno/core/provider/price/PriceFeedService.java index f7a549e0..6f3566d1 100644 --- a/core/src/main/java/haveno/core/provider/price/PriceFeedService.java +++ b/core/src/main/java/haveno/core/provider/price/PriceFeedService.java @@ -33,7 +33,6 @@ import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.provider.PriceHttpClient; import haveno.core.provider.ProvidersRepository; -import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.user.Preferences; import haveno.network.http.HttpClient; @@ -144,15 +143,17 @@ public class PriceFeedService { public void awaitExternalPrices() { CountDownLatch latch = new CountDownLatch(1); ChangeListener listener = (observable, oldValue, newValue) -> { - if (hasExternalPrices() && latch.getCount() != 0) latch.countDown(); + if (hasExternalPrices()) UserThread.execute(() -> latch.countDown()); }; - updateCounter.addListener(listener); - if (hasExternalPrices()) { - updateCounter.removeListener(listener); - return; + UserThread.execute(() -> updateCounter.addListener(listener)); + if (hasExternalPrices()) UserThread.execute(() -> latch.countDown()); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + UserThread.execute(() -> updateCounter.removeListener(listener)); } - HavenoUtils.awaitLatch(latch); - updateCounter.removeListener(listener); } public boolean hasExternalPrices() { @@ -377,17 +378,21 @@ public class PriceFeedService { */ public synchronized Map requestAllPrices() throws ExecutionException, InterruptedException, TimeoutException, CancellationException { CountDownLatch latch = new CountDownLatch(1); - ChangeListener listener = (observable, oldValue, newValue) -> { if (latch.getCount() != 0) latch.countDown(); }; - updateCounter.addListener(listener); + ChangeListener listener = (observable, oldValue, newValue) -> latch.countDown(); + UserThread.execute(() -> updateCounter.addListener(listener)); requestAllPricesError = null; requestPrices(); UserThread.runAfter(() -> { - if (latch.getCount() == 0) return; - requestAllPricesError = "Timeout fetching market prices within 20 seconds"; - latch.countDown(); + if (latch.getCount() > 0) requestAllPricesError = "Timeout fetching market prices within 20 seconds"; + UserThread.execute(() -> latch.countDown()); }, 20); - HavenoUtils.awaitLatch(latch); - updateCounter.removeListener(listener); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + UserThread.execute(() -> updateCounter.removeListener(listener)); + } if (requestAllPricesError != null) throw new RuntimeException(requestAllPricesError); return cache; } From 8ce91aa0c1e11026476a9b59afbdb781bae2b689 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 30 Jun 2024 11:51:56 -0400 Subject: [PATCH 05/58] fix error on shut down updating p2p connections table --- .../java/haveno/core/api/XmrConnectionService.java | 1 + .../main/settings/network/NetworkSettingsView.java | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 50307f9c..0c78fc98 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -99,6 +99,7 @@ public final class XmrConnectionService { private MoneroDaemonInfo lastInfo; private Long syncStartHeight = null; private TaskLooper daemonPollLooper; + @Getter private boolean isShutDownStarted; private List listeners = new ArrayList<>(); diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java index 89c0e750..b3409371 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java @@ -107,7 +107,7 @@ public class NetworkSettingsView extends ActivatableView { private final ClockWatcher clockWatcher; private final WalletsSetup walletsSetup; private final P2PService p2PService; - private final XmrConnectionService connectionManager; + private final XmrConnectionService connectionService; private final ObservableList p2pNetworkListItems = FXCollections.observableArrayList(); private final SortedList p2pSortedList = new SortedList<>(p2pNetworkListItems); @@ -131,7 +131,7 @@ public class NetworkSettingsView extends ActivatableView { @Inject public NetworkSettingsView(WalletsSetup walletsSetup, P2PService p2PService, - XmrConnectionService connectionManager, + XmrConnectionService connectionService, Preferences preferences, XmrNodes xmrNodes, FilterManager filterManager, @@ -141,7 +141,7 @@ public class NetworkSettingsView extends ActivatableView { super(); this.walletsSetup = walletsSetup; this.p2PService = p2PService; - this.connectionManager = connectionManager; + this.connectionService = connectionService; this.preferences = preferences; this.xmrNodes = xmrNodes; this.filterManager = filterManager; @@ -303,10 +303,10 @@ public class NetworkSettingsView extends ActivatableView { rescanOutputsButton.setOnAction(event -> GUIUtil.rescanOutputs(preferences)); - moneroPeersSubscription = EasyBind.subscribe(connectionManager.peerConnectionsProperty(), + moneroPeersSubscription = EasyBind.subscribe(connectionService.peerConnectionsProperty(), this::updateMoneroPeersTable); - moneroBlockHeightSubscription = EasyBind.subscribe(connectionManager.chainHeightProperty(), + moneroBlockHeightSubscription = EasyBind.subscribe(connectionService.chainHeightProperty(), this::updateChainHeightTextField); nodeAddressSubscription = EasyBind.subscribe(p2PService.getNetworkNode().nodeAddressProperty(), @@ -503,6 +503,7 @@ public class NetworkSettingsView extends ActivatableView { } private void updateP2PTable() { + if (connectionService.isShutDownStarted()) return; // ignore if shutting down p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); p2pNetworkListItems.clear(); p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream() From ec9f91e014a337c1baf21eeddf232f1f88672e09 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 30 Jun 2024 11:54:26 -0400 Subject: [PATCH 06/58] fix error on shut down broadcasting to peers --- .../java/haveno/network/p2p/network/NetworkNode.java | 10 ++++++---- .../haveno/network/p2p/peers/BroadcastHandler.java | 8 +++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java b/p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java index d74ef1fd..f24fdf6f 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java @@ -38,6 +38,7 @@ import com.google.common.util.concurrent.SettableFuture; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import lombok.Getter; import java.net.ServerSocket; import java.net.Socket; @@ -82,7 +83,8 @@ public abstract class NetworkNode implements MessageListener { private final ListeningExecutorService sendMessageExecutor; private Server server; - private volatile boolean shutDownInProgress; + @Getter + private volatile boolean isShutDownStarted; // accessed from different threads private final CopyOnWriteArraySet outBoundConnections = new CopyOnWriteArraySet<>(); protected final ObjectProperty nodeAddressProperty = new SimpleObjectProperty<>(); @@ -181,7 +183,7 @@ public abstract class NetworkNode implements MessageListener { try { socket.close(); } catch (Throwable throwable) { - if (!shutDownInProgress) { + if (!isShutDownStarted) { log.error("Error at closing socket " + throwable); } } @@ -362,8 +364,8 @@ public abstract class NetworkNode implements MessageListener { public void shutDown(Runnable shutDownCompleteHandler) { log.info("NetworkNode shutdown started"); - if (!shutDownInProgress) { - shutDownInProgress = true; + if (!isShutDownStarted) { + isShutDownStarted = true; if (server != null) { server.shutDown(); server = null; diff --git a/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java index 238a509b..fca906ff 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java @@ -352,7 +352,13 @@ public class BroadcastHandler implements PeerManager.Listener { sendMessageFutures.stream() .filter(future -> !future.isCancelled() && !future.isDone()) - .forEach(future -> future.cancel(true)); + .forEach(future -> { + try { + future.cancel(true); + } catch (Exception e) { + if (!networkNode.isShutDownStarted()) throw e; + } + }); sendMessageFutures.clear(); peerManager.removeListener(this); From 86e4f7b3f270d4acb5697ca18ff03a915d05b89b Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 5 Jul 2024 08:06:18 -0400 Subject: [PATCH 07/58] retry creating withdraw tx 5 attempts --- .../desktop/main/funds/withdrawal/WithdrawalView.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 c5e40d93..44a66881 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 @@ -99,7 +99,6 @@ public class WithdrawalView extends ActivatableView { private ChangeListener amountListener; private ChangeListener amountFocusListener; private int rowIndex = 0; - private final static int MAX_ATTEMPTS = 3; boolean sendMax = false; /////////////////////////////////////////////////////////////////////////////////////////// @@ -256,7 +255,7 @@ public class WithdrawalView extends ActivatableView { // create tx MoneroTxWallet tx = null; - for (int i = 0; i < MAX_ATTEMPTS; i++) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { try { log.info("Creating withdraw tx"); long startTime = System.currentTimeMillis(); @@ -269,8 +268,8 @@ public class WithdrawalView extends ActivatableView { 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; + log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } From 5d39eecd4fa6a8209d4031be5f07dd0b263d7e57 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 3 Jul 2024 11:00:08 -0400 Subject: [PATCH 08/58] fix 'not enough money' error on reserve exact offer amount #1089 --- .../java/haveno/core/offer/OpenOffer.java | 2 - .../haveno/core/offer/OpenOfferManager.java | 126 +++++++----------- .../main/offer/MutableOfferDataModel.java | 5 - 3 files changed, 49 insertions(+), 84 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index 27f9c829..dcb28259 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -42,7 +42,6 @@ import javafx.beans.property.SimpleObjectProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; @@ -51,7 +50,6 @@ import java.util.List; import java.util.Optional; @EqualsAndHashCode -@Slf4j public final class OpenOffer implements Tradable { public enum State { diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index b9670c2d..5f3dad92 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -96,6 +96,7 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -113,6 +114,7 @@ import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutputQuery; +import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTransferQuery; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxQuery; @@ -852,10 +854,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - public boolean hasAvailableOutput(BigInteger amount) { - return findSplitOutputFundingTx(getOpenOffers(), null, amount, null) != null; - } - /////////////////////////////////////////////////////////////////////////////////////////// // Place offer helpers /////////////////////////////////////////////////////////////////////////////////////////// @@ -929,7 +927,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOffer.isReserveExactAmount()) { // find tx with exact input amount - MoneroTxWallet splitOutputTx = findSplitOutputFundingTx(openOffers, openOffer); + MoneroTxWallet splitOutputTx = getSplitOutputFundingTx(openOffers, openOffer); if (splitOutputTx != null && openOffer.getSplitOutputTxHash() == null) { setSplitOutputTx(openOffer, splitOutputTx); } @@ -965,89 +963,62 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }).start(); } - private MoneroTxWallet findSplitOutputFundingTx(List openOffers, OpenOffer openOffer) { + private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer) { XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); - return findSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex()); + return getSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex()); } - private MoneroTxWallet findSplitOutputFundingTx(List openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) { - List fundingTxs = new ArrayList<>(); - MoneroTxWallet earliestUnscheduledTx = null; + private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) { // return split output tx if already assigned if (openOffer != null && openOffer.getSplitOutputTxHash() != null) { return xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); } - // return earliest tx with exact amount to offer's subaddress if available + // get split output tx to offer's preferred subaddress if (preferredSubaddressIndex != null) { - - // get txs with exact output amount - fundingTxs = xmrWalletService.getTxs(new MoneroTxQuery() - .setIsConfirmed(true) - .setOutputQuery(new MoneroOutputQuery() - .setAccountIndex(0) - .setSubaddressIndex(preferredSubaddressIndex) - .setAmount(reserveAmount) - .setIsSpent(false) - .setIsFrozen(false))); - - // return earliest tx if available - earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); + List fundingTxs = getSplitOutputFundingTxs(reserveAmount, preferredSubaddressIndex); + MoneroTxWallet earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs); if (earliestUnscheduledTx != null) return earliestUnscheduledTx; } - // return if awaiting scheduled tx - if (openOffer.getScheduledTxHashes() != null) return null; - - // get all transactions including from pool - List allTxs = xmrWalletService.getTxs(false); - - if (preferredSubaddressIndex != null) { - - // return earliest tx with exact incoming transfer to fund offer's subaddress if available (since outputs are not available until confirmed) - fundingTxs.clear(); - for (MoneroTxWallet tx : allTxs) { - boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery() - .setIsIncoming(true) - .setAccountIndex(0) - .setSubaddressIndex(preferredSubaddressIndex) - .setAmount(reserveAmount)).size() > 0; - if (hasExactTransfer) fundingTxs.add(tx); - } - earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); - if (earliestUnscheduledTx != null) return earliestUnscheduledTx; - } - - // return earliest tx with exact confirmed output to any subaddress if available - fundingTxs.clear(); - for (MoneroTxWallet tx : allTxs) { - boolean hasExactOutput = tx.getOutputsWallet(new MoneroOutputQuery() - .setAccountIndex(0) - .setAmount(reserveAmount) - .setIsSpent(false) - .setIsFrozen(false)).size() > 0; - if (hasExactOutput) fundingTxs.add(tx); - } - earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); - if (earliestUnscheduledTx != null) return earliestUnscheduledTx; - - // return earliest tx with exact incoming transfer to any subaddress if available (since outputs are not available until confirmed) - fundingTxs.clear(); - for (MoneroTxWallet tx : allTxs) { - boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery() - .setIsIncoming(true) - .setAccountIndex(0) - .setAmount(reserveAmount)).size() > 0; - if (hasExactTransfer) fundingTxs.add(tx); - } - return getEarliestUnscheduledTx(openOffers, fundingTxs); + // get split output tx to any subaddress + List fundingTxs = getSplitOutputFundingTxs(reserveAmount, null); + return getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs); } - private MoneroTxWallet getEarliestUnscheduledTx(List openOffers, List txs) { + private List getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) { + List splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false)); + Set removeTxs = new HashSet(); + for (MoneroTxWallet tx : splitOutputTxs) { + if (tx.getOutputs() != null) { // outputs not available until first confirmation + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (output.isSpent() || output.isFrozen()) removeTxs.add(tx); + } + } + if (!hasExactAmount(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); + } + splitOutputTxs.removeAll(removeTxs); + return splitOutputTxs; + } + + private boolean hasExactAmount(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { + boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery() + .setAccountIndex(0) + .setSubaddressIndex(preferredSubaddressIndex) + .setAmount(amount)).size() > 0); + if (hasExactOutput) return true; + boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery() + .setAccountIndex(0) + .setSubaddressIndex(preferredSubaddressIndex) + .setAmount(amount)).size() > 0); + return hasExactTransfer; + } + + private MoneroTxWallet getEarliestUnscheduledTx(List openOffers, OpenOffer excludeOpenOffer, List txs) { MoneroTxWallet earliestUnscheduledTx = null; for (MoneroTxWallet tx : txs) { - if (isTxScheduled(openOffers, tx.getHash())) continue; + if (isTxScheduledByOtherOffer(openOffers, excludeOpenOffer, tx.getHash())) continue; if (earliestUnscheduledTx == null || (earliestUnscheduledTx.getNumConfirmations() < tx.getNumConfirmations())) earliestUnscheduledTx = tx; } return earliestUnscheduledTx; @@ -1121,7 +1092,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe List scheduledTxHashes = new ArrayList(); BigInteger scheduledAmount = BigInteger.ZERO; for (MoneroTxWallet lockedTx : lockedTxs) { - if (isTxScheduled(openOffers, lockedTx.getHash())) continue; + if (isTxScheduledByOtherOffer(openOffers, openOffer, lockedTx.getHash())) continue; if (lockedTx.getIncomingTransfers() == null || lockedTx.getIncomingTransfers().isEmpty()) continue; scheduledTxHashes.add(lockedTx.getHash()); for (MoneroIncomingTransfer transfer : lockedTx.getIncomingTransfers()) { @@ -1154,11 +1125,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return scheduledAmount; } - private boolean isTxScheduled(List openOffers, String txHash) { - for (OpenOffer openOffer : openOffers) { - if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue; - if (openOffer.getScheduledTxHashes() == null) continue; - for (String scheduledTxHash : openOffer.getScheduledTxHashes()) { + private boolean isTxScheduledByOtherOffer(List openOffers, OpenOffer openOffer, String txHash) { + for (OpenOffer otherOffer : openOffers) { + if (otherOffer == openOffer) continue; + if (otherOffer.getState() != OpenOffer.State.SCHEDULED) continue; + if (otherOffer.getScheduledTxHashes() == null) continue; + for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) { if (txHash.equals(scheduledTxHash)) return true; } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index f94ccbd3..e6d1d9d4 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -466,11 +466,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } } - public boolean hasAvailableSplitOutput() { - BigInteger reserveAmount = totalToPay.get(); - return openOfferManager.hasAvailableOutput(reserveAmount); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// From 31ce183c839b9c3d0279abfdbf23fb573c45357d Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 8 Jul 2024 09:40:45 -0400 Subject: [PATCH 09/58] fix invalid res lookup for UpholdForm --- .../haveno/desktop/components/paymentmethods/UpholdForm.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java index 4ef493cf..5443139d 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java @@ -112,7 +112,7 @@ public class UpholdForm extends PaymentMethodForm { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(upholdAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), - Res.get(upholdAccount.getAccountOwner())); + upholdAccount.getAccountOwner()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), upholdAccount.getAccountId()).second; field.setMouseTransparent(false); From 8bf8144709fd550c0d7395d920de97c161156f54 Mon Sep 17 00:00:00 2001 From: walkerp07 Date: Sat, 6 Jul 2024 20:01:54 -0400 Subject: [PATCH 10/58] Add an "extra_info" text area for PayPal --- .../main/java/haveno/core/offer/Offer.java | 2 ++ .../java/haveno/core/offer/OfferPayload.java | 1 + .../java/haveno/core/offer/OfferUtil.java | 6 ++++ .../haveno/core/payment/PayPalAccount.java | 9 ++++++ .../payment/payload/PayPalAccountPayload.java | 11 +++++-- .../components/paymentmethods/PayPalForm.java | 29 ++++++++++++++++++- .../main/offer/takeoffer/TakeOfferView.java | 21 +++++++++++++- .../overlays/windows/OfferDetailsWindow.java | 5 +++- proto/src/main/proto/pb.proto | 1 + 9 files changed, 79 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 5c2f6eb5..63d9fd02 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -410,6 +410,8 @@ public class Offer implements NetworkPayload, PersistablePayload { return getExtraDataMap().get(OfferPayload.PAY_BY_MAIL_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO); + else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAYPAL_EXTRA_INFO)) + return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO); else return ""; } diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index 1a194a1a..6f69dbeb 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -100,6 +100,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay public static final String F2F_EXTRA_INFO = "f2fExtraInfo"; public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo"; public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo"; + public static final String PAYPAL_EXTRA_INFO = "paypalExtraInfo"; // Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 6d314fe2..a097bf96 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -40,6 +40,7 @@ import static haveno.core.offer.OfferPayload.CAPABILITIES; import static haveno.core.offer.OfferPayload.F2F_CITY; import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO; import static haveno.core.offer.OfferPayload.PAY_BY_MAIL_EXTRA_INFO; +import static haveno.core.offer.OfferPayload.PAYPAL_EXTRA_INFO; import static haveno.core.offer.OfferPayload.REFERRAL_ID; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE; @@ -47,6 +48,7 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE; import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.PayByMailAccount; +import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; @@ -200,6 +202,10 @@ public class OfferUtil { extraDataMap.put(PAY_BY_MAIL_EXTRA_INFO, ((PayByMailAccount) paymentAccount).getExtraInfo()); } + if (paymentAccount instanceof PayPalAccount) { + extraDataMap.put(PAYPAL_EXTRA_INFO, ((PayPalAccount) paymentAccount).getExtraInfo()); + } + if (paymentAccount instanceof AustraliaPayidAccount) { extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo()); } diff --git a/core/src/main/java/haveno/core/payment/PayPalAccount.java b/core/src/main/java/haveno/core/payment/PayPalAccount.java index 04c8a364..c7ec1518 100644 --- a/core/src/main/java/haveno/core/payment/PayPalAccount.java +++ b/core/src/main/java/haveno/core/payment/PayPalAccount.java @@ -62,6 +62,7 @@ public final class PayPalAccount extends PaymentAccount { PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_USERNAME, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.SALT); public PayPalAccount() { @@ -91,4 +92,12 @@ public final class PayPalAccount extends PaymentAccount { public String getEmailOrMobileNrOrUsername() { return ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername(); } + + public void setExtraInfo(String extraInfo) { + ((PayPalAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); + } + + public String getExtraInfo() { + return ((PayPalAccountPayload) paymentAccountPayload).getExtraInfo(); + } } diff --git a/core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java b/core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java index 31765f49..76e4f878 100644 --- a/core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java +++ b/core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java @@ -36,6 +36,7 @@ import java.util.Map; @Slf4j public final class PayPalAccountPayload extends PaymentAccountPayload { private String emailOrMobileNrOrUsername = ""; + private String extraInfo = ""; public PayPalAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -48,6 +49,7 @@ public final class PayPalAccountPayload extends PaymentAccountPayload { private PayPalAccountPayload(String paymentMethod, String id, String emailOrMobileNrOrUsername, + String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -56,13 +58,15 @@ public final class PayPalAccountPayload extends PaymentAccountPayload { excludeFromJsonDataMap); this.emailOrMobileNrOrUsername = emailOrMobileNrOrUsername; + this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPaypalAccountPayload(protobuf.PayPalAccountPayload.newBuilder() - .setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername)) + .setExtraInfo(extraInfo) + .setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername)) .build(); } @@ -70,6 +74,7 @@ public final class PayPalAccountPayload extends PaymentAccountPayload { return new PayPalAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPaypalAccountPayload().getEmailOrMobileNrOrUsername(), + proto.getPaypalAccountPayload().getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -80,8 +85,8 @@ public final class PayPalAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.getWithCol("payment.email.mobile.username") + " " - + emailOrMobileNrOrUsername; + return Res.getWithCol("payment.email.mobile.username") + " "+ emailOrMobileNrOrUsername + "\n" + + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo+ "\n"; } @Override diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java index b3c9833d..e382b421 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java @@ -17,6 +17,7 @@ package haveno.desktop.components.paymentmethods; +import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PayPalAccount; @@ -29,13 +30,16 @@ import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; -import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; +import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; public class PayPalForm extends PaymentMethodForm { private final PayPalAccount paypalAccount; @@ -43,6 +47,13 @@ public class PayPalForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername()); + + PayPalAccountPayload payId = (PayPalAccountPayload) paymentAccountPayload; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + textExtraInfo.setMinHeight(70); + textExtraInfo.setEditable(false); + textExtraInfo.setText(payId.getExtraInfo()); + return gridRow; } @@ -66,6 +77,16 @@ public class PayPalForm extends PaymentMethodForm { paypalAccount.setEmailOrMobileNrOrUsername(newValue.trim()); updateFromInputs(); }); + + TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + extraTextArea.setMinHeight(70); + ((JFXTextArea) extraTextArea).setLabelFloat(false); + extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + paypalAccount.setExtraInfo(newValue); + updateFromInputs(); + }); + addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); @@ -99,6 +120,12 @@ public class PayPalForm extends PaymentMethodForm { TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), paypalAccount.getEmailOrMobileNrOrUsername()).second; + + TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; + textAreaExtra.setText(paypalAccount.getExtraInfo()); + textAreaExtra.setMinHeight(70); + textAreaExtra.setEditable(false); + field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 2b8899c3..e64e1a6a 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -158,7 +158,8 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountWarningDisplayed = new HashMap<>(); private boolean offerDetailsWindowDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, - takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, australiaPayidWarningDisplayed; + takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, + australiaPayidWarningDisplayed, paypalWarningDisplayed; private SimpleBooleanProperty errorPopupDisplayed; private ChangeListener amountFocusedListener, getShowWalletFundedNotificationListener; @@ -268,6 +269,7 @@ public class TakeOfferView extends ActivatableViewAndModel { + new GenericMessageWindow() + .preamble(Res.get("payment.tradingRestrictions")) + .instruction(offer.getExtraInfo()) + .actionButtonText(Res.get("shared.iConfirm")) + .closeButtonText(Res.get("shared.close")) + .width(Layout.INITIAL_WINDOW_WIDTH) + .onClose(() -> close(false)) + .show(); + }, 500, TimeUnit.MILLISECONDS); + } + } + private Tuple2 getTradeInputBox(HBox amountValueBox, String promptText) { Label descriptionLabel = new AutoTooltipLabel(promptText); descriptionLabel.setId("input-description-label"); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index 0692162e..30ed30e0 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -174,7 +174,10 @@ public class OfferDetailsWindow extends Overlay { List acceptedCountryCodes = offer.getAcceptedCountryCodes(); boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); boolean isF2F = offer.getPaymentMethod().equals(PaymentMethod.F2F); - boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || offer.getPaymentMethod().equals(PaymentMethod.PAY_BY_MAIL) || offer.getPaymentMethod().equals(PaymentMethod.AUSTRALIA_PAYID); + boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || + offer.getPaymentMethod().equals(PaymentMethod.PAY_BY_MAIL) || + offer.getPaymentMethod().equals(PaymentMethod.AUSTRALIA_PAYID)|| + offer.getPaymentMethod().equals(PaymentMethod.PAYPAL_ID); if (!takeOfferHandlerOptional.isPresent()) rows++; if (showAcceptedBanks) diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 8e988bbe..f50bebfb 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1072,6 +1072,7 @@ message VenmoAccountPayload { } message PayPalAccountPayload { string email_or_mobile_nr_or_username = 1; + string extra_info = 2; } message PopmoneyAccountPayload { From 05b00727a50a71699dfb53267c28822eb13463a5 Mon Sep 17 00:00:00 2001 From: walkerp07 Date: Mon, 8 Jul 2024 03:39:23 -0400 Subject: [PATCH 11/58] Add an "extra_info" text area for CashApp --- .../main/java/haveno/core/offer/Offer.java | 2 ++ .../java/haveno/core/offer/OfferPayload.java | 1 + .../java/haveno/core/offer/OfferUtil.java | 6 +++++ .../haveno/core/payment/CashAppAccount.java | 9 +++++++ .../payload/CashAppAccountPayload.java | 12 +++++++-- .../paymentmethods/CashAppForm.java | 27 +++++++++++++++++++ .../main/offer/takeoffer/TakeOfferView.java | 20 +++++++++++++- .../overlays/windows/OfferDetailsWindow.java | 3 ++- proto/src/main/proto/pb.proto | 1 + 9 files changed, 77 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 63d9fd02..675fbeb5 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -412,6 +412,8 @@ public class Offer implements NetworkPayload, PersistablePayload { return getExtraDataMap().get(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAYPAL_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO); + else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO)) + return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO); else return ""; } diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index 6f69dbeb..a1e30d8d 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -94,6 +94,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay // Keys for extra map // Only set for traditional offers public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash"; + public static final String CASHAPP_EXTRA_INFO = "cashAppExtraInfo"; public static final String REFERRAL_ID = "referralId"; // Only used in payment method F2F public static final String F2F_CITY = "f2fCity"; diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index a097bf96..72593ab5 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -37,6 +37,7 @@ import haveno.core.monetary.Volume; import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH; import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO; import static haveno.core.offer.OfferPayload.CAPABILITIES; +import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO; import static haveno.core.offer.OfferPayload.F2F_CITY; import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO; import static haveno.core.offer.OfferPayload.PAY_BY_MAIL_EXTRA_INFO; @@ -46,6 +47,7 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE; import haveno.core.payment.AustraliaPayidAccount; +import haveno.core.payment.CashAppAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayPalAccount; @@ -206,6 +208,10 @@ public class OfferUtil { extraDataMap.put(PAYPAL_EXTRA_INFO, ((PayPalAccount) paymentAccount).getExtraInfo()); } + if (paymentAccount instanceof CashAppAccount) { + extraDataMap.put(CASHAPP_EXTRA_INFO, ((CashAppAccount) paymentAccount).getExtraInfo()); + } + if (paymentAccount instanceof AustraliaPayidAccount) { extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo()); } diff --git a/core/src/main/java/haveno/core/payment/CashAppAccount.java b/core/src/main/java/haveno/core/payment/CashAppAccount.java index eea4d26c..250f1c57 100644 --- a/core/src/main/java/haveno/core/payment/CashAppAccount.java +++ b/core/src/main/java/haveno/core/payment/CashAppAccount.java @@ -39,6 +39,7 @@ public final class CashAppAccount extends PaymentAccount { PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_CASHTAG, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.SALT); public CashAppAccount() { @@ -67,4 +68,12 @@ public final class CashAppAccount extends PaymentAccount { public String getEmailOrMobileNrOrCashtag() { return ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag(); } + + public void setExtraInfo(String extraInfo) { + ((CashAppAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); + } + + public String getExtraInfo() { + return ((CashAppAccountPayload) paymentAccountPayload).getExtraInfo(); + } } diff --git a/core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java b/core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java index d9f74940..bec0f340 100644 --- a/core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java +++ b/core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java @@ -36,6 +36,7 @@ import java.util.Map; @Slf4j public final class CashAppAccountPayload extends PaymentAccountPayload { private String emailOrMobileNrOrCashtag = ""; + private String extraInfo = ""; public CashAppAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -48,6 +49,7 @@ public final class CashAppAccountPayload extends PaymentAccountPayload { private CashAppAccountPayload(String paymentMethod, String id, String emailOrMobileNrOrCashtag, + String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -56,13 +58,15 @@ public final class CashAppAccountPayload extends PaymentAccountPayload { excludeFromJsonDataMap); this.emailOrMobileNrOrCashtag = emailOrMobileNrOrCashtag; + this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setCashAppAccountPayload(protobuf.CashAppAccountPayload.newBuilder() - .setEmailOrMobileNrOrCashtag(emailOrMobileNrOrCashtag)) + .setExtraInfo(extraInfo) + .setEmailOrMobileNrOrCashtag(emailOrMobileNrOrCashtag)) .build(); } @@ -70,6 +74,7 @@ public final class CashAppAccountPayload extends PaymentAccountPayload { return new CashAppAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getCashAppAccountPayload().getEmailOrMobileNrOrCashtag(), + proto.getCashAppAccountPayload().getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -81,7 +86,10 @@ public final class CashAppAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email.mobile.cashtag") + " " + emailOrMobileNrOrCashtag; + return Res.get(paymentMethodId) + " - " + + Res.getWithCol("payment.email.mobile.cashtag") + + " " + emailOrMobileNrOrCashtag + "\n" + + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo+ "\n"; } @Override diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java index ac54d7a2..529af439 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java @@ -17,6 +17,7 @@ package haveno.desktop.components.paymentmethods; +import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.CashAppAccount; @@ -29,6 +30,7 @@ import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; @@ -36,6 +38,8 @@ import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; +import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; public class CashAppForm extends PaymentMethodForm { private final CashAppAccount cashAppAccount; @@ -43,6 +47,13 @@ public class CashAppForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag()); + + CashAppAccountPayload payId = (CashAppAccountPayload) paymentAccountPayload; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + textExtraInfo.setMinHeight(70); + textExtraInfo.setEditable(false); + textExtraInfo.setText(payId.getExtraInfo()); + return gridRow; } @@ -66,6 +77,16 @@ public class CashAppForm extends PaymentMethodForm { cashAppAccount.setEmailOrMobileNrOrCashtag(newValue.trim()); updateFromInputs(); }); + + TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + extraTextArea.setMinHeight(70); + ((JFXTextArea) extraTextArea).setLabelFloat(false); + extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + cashAppAccount.setExtraInfo(newValue); + updateFromInputs(); + }); + addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); @@ -96,6 +117,12 @@ public class CashAppForm extends PaymentMethodForm { addAccountNameTextFieldWithAutoFillToggleButton(); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), cashAppAccount.getEmailOrMobileNrOrCashtag()).second; + + TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; + textAreaExtra.setText(cashAppAccount.getExtraInfo()); + textAreaExtra.setMinHeight(70); + textAreaExtra.setEditable(false); + field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index e64e1a6a..98233d26 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -159,7 +159,7 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountWarningDisplayed = new HashMap<>(); private boolean offerDetailsWindowDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, - australiaPayidWarningDisplayed, paypalWarningDisplayed; + australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed; private SimpleBooleanProperty errorPopupDisplayed; private ChangeListener amountFocusedListener, getShowWalletFundedNotificationListener; @@ -270,6 +270,7 @@ public class TakeOfferView extends ActivatableViewAndModel { + new GenericMessageWindow() + .preamble(Res.get("payment.tradingRestrictions")) + .instruction(offer.getExtraInfo()) + .actionButtonText(Res.get("shared.iConfirm")) + .closeButtonText(Res.get("shared.close")) + .width(Layout.INITIAL_WINDOW_WIDTH) + .onClose(() -> close(false)) + .show(); + }, 500, TimeUnit.MILLISECONDS); + } + } + private Tuple2 getTradeInputBox(HBox amountValueBox, String promptText) { Label descriptionLabel = new AutoTooltipLabel(promptText); descriptionLabel.setId("input-description-label"); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index 30ed30e0..e93dc647 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -177,7 +177,8 @@ public class OfferDetailsWindow extends Overlay { boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || offer.getPaymentMethod().equals(PaymentMethod.PAY_BY_MAIL) || offer.getPaymentMethod().equals(PaymentMethod.AUSTRALIA_PAYID)|| - offer.getPaymentMethod().equals(PaymentMethod.PAYPAL_ID); + offer.getPaymentMethod().equals(PaymentMethod.PAYPAL_ID)|| + offer.getPaymentMethod().equals(PaymentMethod.CASH_APP_ID); if (!takeOfferHandlerOptional.isPresent()) rows++; if (showAcceptedBanks) diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index f50bebfb..4a24f413 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1061,6 +1061,7 @@ message UpholdAccountPayload { message CashAppAccountPayload { string email_or_mobile_nr_or_cashtag = 1; + string extra_info = 2; } message MoneyBeamAccountPayload { From 7c2af064a3626fb5ebae066549a154beaabe5440 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 8 Jul 2024 11:43:18 -0400 Subject: [PATCH 12/58] fix case for paypal extra info --- core/src/main/java/haveno/core/offer/OfferPayload.java | 2 +- .../haveno/desktop/main/offer/takeoffer/TakeOfferView.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index a1e30d8d..2f377bab 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -101,7 +101,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay public static final String F2F_EXTRA_INFO = "f2fExtraInfo"; public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo"; public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo"; - public static final String PAYPAL_EXTRA_INFO = "paypalExtraInfo"; + public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo"; // Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 98233d26..1b0867ef 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -269,7 +269,7 @@ public class TakeOfferView extends ActivatableViewAndModel Date: Mon, 8 Jul 2024 11:13:37 -0400 Subject: [PATCH 13/58] set selected currency when account has single currency --- .../traditionalaccounts/TraditionalAccountsDataModel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java index d34857a1..b72b89fe 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java @@ -108,6 +108,7 @@ class TraditionalAccountsDataModel extends ActivatableDataModel { TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); List tradeCurrencies = paymentAccount.getTradeCurrencies(); if (singleTradeCurrency != null) { + paymentAccount.setSelectedTradeCurrency(singleTradeCurrency); if (singleTradeCurrency instanceof TraditionalCurrency) preferences.addTraditionalCurrency((TraditionalCurrency) singleTradeCurrency); else From 8e3e91c7cc0032f6e6a2568a79eed13e7bbf909e Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 10 Jul 2024 08:39:14 -0400 Subject: [PATCH 14/58] remove bitcoin from makefile #1120 --- Makefile | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Makefile b/Makefile index bcd9b1bc..7d5f5abb 100644 --- a/Makefile +++ b/Makefile @@ -40,24 +40,9 @@ deploy: screen -S localnet -X screen -t $$target; \ screen -S localnet -p $$target -X stuff "make $$target\n"; \ done; - # give bitcoind rpc server time to start + # give time to start sleep 5 -bitcoind: - ./.localnet/bitcoind \ - -regtest \ - -peerbloomfilters=1 \ - -datadir=.localnet/ \ - -rpcuser=haveno \ - -rpcpassword=1234 \ - -btc-blocks: - ./.localnet/bitcoin-cli \ - -regtest \ - -rpcuser=haveno \ - -rpcpassword=1234 \ - generatetoaddress 101 bcrt1q6j90vywv8x7eyevcnn2tn2wrlg3vsjlsvt46qz - .PHONY: build seednode localnet # Local network From db6cb237bff8435cb156a344b7a554b5bde772a5 Mon Sep 17 00:00:00 2001 From: fa2a5qj3 <174058787+fa2a5qj3@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:58:05 -0400 Subject: [PATCH 15/58] randomize completed trade info, fixes #1099 --- .../trade/statistics/TradeStatistics3.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java index 75db5880..dbaa8800 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java @@ -54,6 +54,8 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; @@ -88,13 +90,27 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl Offer offer = checkNotNull(trade.getOffer()); return new TradeStatistics3(offer.getCurrencyCode(), trade.getPrice().getValue(), - trade.getAmount().longValueExact(), + fuzzTradeAmount(trade.getAmount().longValueExact()), offer.getPaymentMethod().getId(), - trade.getTakeOfferDate().getTime(), + fuzzTradeDate(trade.getTakeOfferDate().getTime()), truncatedArbitratorNodeAddress, extraDataMap); } + private static long fuzzTradeAmount(long exactAmount) { // randomize completed trade info #1099 + long adjustedAmount = (long) ThreadLocalRandom.current().nextDouble( + exactAmount * 0.95, exactAmount * 1.05); + log.info("fuzzed trade amount from {} to {}", exactAmount, adjustedAmount); + return adjustedAmount; + } + + private static long fuzzTradeDate(long originalTimestamp) { // randomize completed trade info #1099 + long adjustedTimestamp = ThreadLocalRandom.current().nextLong( + originalTimestamp-TimeUnit.HOURS.toMillis(24), originalTimestamp); + log.info("fuzzed trade datestamp from {} to {}", new Date(originalTimestamp), new Date(adjustedTimestamp)); + return adjustedTimestamp; + } + // This enum must not change the order as we use the ordinal for storage to reduce data size. // The payment method string can be quite long and would consume 15% more space. // When we get a new payment method we can add it to the enum at the end. Old users would add it as string if not From b0c73d1b39b1e49e07e714deafde3af58be441e1 Mon Sep 17 00:00:00 2001 From: Shortwavesurfer2009 Date: Wed, 10 Jul 2024 12:54:15 -0400 Subject: [PATCH 16/58] update to ipv6 address in seednode service --- scripts/deployment/haveno-seednode.service | 4 ++-- scripts/deployment/haveno-seednode2.service | 4 ++-- seednode/haveno-seednode.service | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/deployment/haveno-seednode.service b/scripts/deployment/haveno-seednode.service index 3e7ced65..fdb32569 100644 --- a/scripts/deployment/haveno-seednode.service +++ b/scripts/deployment/haveno-seednode.service @@ -13,7 +13,7 @@ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_ --nodePort=2002\ --appName=haveno-XMR_STAGENET_Seed_2002\ # --logLevel=trace\ - --xmrNode=http://127.0.0.1:38088\ + --xmrNode=http://[::1]:38088\ --xmrNodeUsername=admin\ --xmrNodePassword=password @@ -33,4 +33,4 @@ RestrictSUIDSGID=true LimitRSS=2000000000 [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/scripts/deployment/haveno-seednode2.service b/scripts/deployment/haveno-seednode2.service index d5937f2a..7b6c139a 100644 --- a/scripts/deployment/haveno-seednode2.service +++ b/scripts/deployment/haveno-seednode2.service @@ -13,7 +13,7 @@ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_ --nodePort=3003\ --appName=haveno-XMR_STAGENET_Seed_3003\ # --logLevel=trace\ - --xmrNode=http://127.0.0.1:38088\ + --xmrNode=http://[::1]:38088\ --xmrNodeUsername=admin\ --xmrNodePassword=password @@ -33,4 +33,4 @@ RestrictSUIDSGID=true LimitRSS=2000000000 [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/seednode/haveno-seednode.service b/seednode/haveno-seednode.service index 7551c54f..5bbae833 100644 --- a/seednode/haveno-seednode.service +++ b/seednode/haveno-seednode.service @@ -13,6 +13,7 @@ ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ --useDevPrivilegeKeys=false\ --nodePort=2002\ --appName=haveno-XMR_STAGENET_Seed_2002 + --xmrNode=[::1]:38088 ExecStop=/bin/kill ${MAINPID} Restart=always From 9747b20a276f4823a18373b988f80ea1d04a4b88 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 12 Jul 2024 14:36:14 -0400 Subject: [PATCH 17/58] show error message on error confirming payment received --- .../portfolio/pendingtrades/steps/seller/SellerStep3View.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index 56617dd6..a492c744 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -450,7 +450,7 @@ public class SellerStep3View extends TradeStepView { model.dataModel.onPaymentReceived(() -> { }, errorMessage -> { busyAnimation.stop(); - new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show(); + new Popup().warning(Res.get("popup.warning.sendMsgFailed") + "\n\n" + errorMessage).show(); confirmButton.setDisable(!confirmPaymentReceivedPermitted()); UserThread.execute(() -> statusLabel.setText("Error confirming payment received.")); }); From 082c8c42906df96bafbae7e1195c024be4a2c7f9 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 13 Jul 2024 07:37:48 -0400 Subject: [PATCH 18/58] Revert "randomize completed trade info, fixes #1099" This reverts commit db6cb237bff8435cb156a344b7a554b5bde772a5. --- .../trade/statistics/TradeStatistics3.java | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java index dbaa8800..75db5880 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java @@ -54,8 +54,6 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; @@ -90,27 +88,13 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl Offer offer = checkNotNull(trade.getOffer()); return new TradeStatistics3(offer.getCurrencyCode(), trade.getPrice().getValue(), - fuzzTradeAmount(trade.getAmount().longValueExact()), + trade.getAmount().longValueExact(), offer.getPaymentMethod().getId(), - fuzzTradeDate(trade.getTakeOfferDate().getTime()), + trade.getTakeOfferDate().getTime(), truncatedArbitratorNodeAddress, extraDataMap); } - private static long fuzzTradeAmount(long exactAmount) { // randomize completed trade info #1099 - long adjustedAmount = (long) ThreadLocalRandom.current().nextDouble( - exactAmount * 0.95, exactAmount * 1.05); - log.info("fuzzed trade amount from {} to {}", exactAmount, adjustedAmount); - return adjustedAmount; - } - - private static long fuzzTradeDate(long originalTimestamp) { // randomize completed trade info #1099 - long adjustedTimestamp = ThreadLocalRandom.current().nextLong( - originalTimestamp-TimeUnit.HOURS.toMillis(24), originalTimestamp); - log.info("fuzzed trade datestamp from {} to {}", new Date(originalTimestamp), new Date(adjustedTimestamp)); - return adjustedTimestamp; - } - // This enum must not change the order as we use the ordinal for storage to reduce data size. // The payment method string can be quite long and would consume 15% more space. // When we get a new payment method we can add it to the enum at the end. Old users would add it as string if not From 255bd33c471061bb7929843005e9751552396a94 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 13 Jul 2024 08:37:44 -0400 Subject: [PATCH 19/58] increase chat message limit to 4 per minute --- .../src/main/java/haveno/daemon/grpc/GrpcTradesService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index 70c2c355..e521a7ee 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -258,8 +258,8 @@ class GrpcTradesService extends TradesImplBase { put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES)); - put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } From f8522179456366d1ca2b61a32150c55e704a9e89 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 13 Jul 2024 09:01:52 -0400 Subject: [PATCH 20/58] adjust presentation of extra info in buyer step screen --- .../desktop/components/paymentmethods/AustraliaPayidForm.java | 4 ++-- .../haveno/desktop/components/paymentmethods/CashAppForm.java | 2 +- .../haveno/desktop/components/paymentmethods/PayPalForm.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java index c830b889..234b5e88 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java @@ -48,11 +48,11 @@ public class AustraliaPayidForm extends PaymentMethodForm { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), ((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName()); - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), + addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.payid"), ((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid()); AustraliaPayidAccountPayload payId = (AustraliaPayidAccountPayload) paymentAccountPayload; - TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(payId.getExtraInfo()); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java index 529af439..60fe7127 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java @@ -49,7 +49,7 @@ public class CashAppForm extends PaymentMethodForm { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag()); CashAppAccountPayload payId = (CashAppAccountPayload) paymentAccountPayload; - TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(payId.getExtraInfo()); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java index e382b421..8e0d48ee 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java @@ -49,7 +49,7 @@ public class PayPalForm extends PaymentMethodForm { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername()); PayPalAccountPayload payId = (PayPalAccountPayload) paymentAccountPayload; - TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(payId.getExtraInfo()); From c2b816e5f08c2e2bbcbde6a63c4b8e92617a0b2e Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 9 Jul 2024 09:27:59 -0400 Subject: [PATCH 21/58] log when offer's signing arbitrator is unavailable --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 5f3dad92..e18d06ae 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1693,7 +1693,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // determine if offer is valid boolean isValid = true; Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); - if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { + if (arbitrator == null) { + log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId()); + isValid = false; + } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); isValid = false; } From c1c8c6fa854313e45158cf4272da2b94e9c55b5b Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 9 Jul 2024 11:24:42 -0400 Subject: [PATCH 22/58] remove extra quote from java opts --- seednode/haveno.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seednode/haveno.env b/seednode/haveno.env index 31e07d7a..9b7ff903 100644 --- a/seednode/haveno.env +++ b/seednode/haveno.env @@ -5,7 +5,7 @@ JAVA_HOME=/usr/lib/jvm/openjdk-21 # java memory and remote management options -JAVA_OPTS="-Xms4096M -Xmx4096M" -XX:+ExitOnOutOfMemoryError" +JAVA_OPTS="-Xms4096M -Xmx4096M -XX:+ExitOnOutOfMemoryError" # use external tor (change to -1 for internal tor binary) HAVENO_EXTERNAL_TOR_PORT=9051 From 4a6fcfae8475d0dfbc9eea7e8e6588b9f0df6218 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 9 Jul 2024 13:08:04 -0400 Subject: [PATCH 23/58] limit logging of invalid connection requests --- .../network/p2p/network/Connection.java | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/network/Connection.java b/p2p/src/main/java/haveno/network/p2p/network/Connection.java index 137e84a5..406cfe25 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -170,6 +170,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private final Capabilities capabilities = new Capabilities(); + // throttle logs of reported invalid requests + private static long lastLoggedInvalidRequestReport = 0; + private static int unloggedInvalidRequestReports = 0; + private static final long LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS = 60000; // log invalid request reports once every 60s + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @@ -606,35 +611,55 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { */ public boolean reportInvalidRequest(RuleViolation ruleViolation) { - log.info("We got reported the ruleViolation {} at connection with address{} and uid {}", ruleViolation, this.getPeersNodeAddressProperty(), this.getUid()); + return Connection.reportInvalidRequest(this, ruleViolation); + } + + private static synchronized boolean reportInvalidRequest(Connection connection, RuleViolation ruleViolation) { + + // determine if report should be logged to avoid spamming the logs + boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReport > LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS; + + // count the number of unlogged reports since last log entry + if (!logReport) unloggedInvalidRequestReports++; + + // handle report + if (logReport) log.info("We got reported the ruleViolation {} at connection with address {} and uid {}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid()); int numRuleViolations; - numRuleViolations = ruleViolations.getOrDefault(ruleViolation, 0); - + numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0); numRuleViolations++; - ruleViolations.put(ruleViolation, numRuleViolations); - + connection.ruleViolations.put(ruleViolation, numRuleViolations); if (numRuleViolations >= ruleViolation.maxTolerance) { - log.warn("We close connection as we received too many corrupt requests. " + + if (logReport) log.warn("We close connection as we received too many corrupt requests. " + "ruleViolations={} " + - "connection with address{} and uid {}", ruleViolations, peersNodeAddressProperty, uid); - this.ruleViolation = ruleViolation; + "connection with address {} and uid {}", connection.ruleViolations, connection.peersNodeAddressProperty, connection.uid); + connection.ruleViolation = ruleViolation; if (ruleViolation == RuleViolation.PEER_BANNED) { - log.debug("We close connection due RuleViolation.PEER_BANNED. peersNodeAddress={}", getPeersNodeAddressOptional()); - shutDown(CloseConnectionReason.PEER_BANNED); + if (logReport) log.debug("We close connection due RuleViolation.PEER_BANNED. peersNodeAddress={}", connection.getPeersNodeAddressOptional()); + connection.shutDown(CloseConnectionReason.PEER_BANNED); } else if (ruleViolation == RuleViolation.INVALID_CLASS) { - log.warn("We close connection due RuleViolation.INVALID_CLASS"); - shutDown(CloseConnectionReason.INVALID_CLASS_RECEIVED); + if (logReport) log.warn("We close connection due RuleViolation.INVALID_CLASS"); + connection.shutDown(CloseConnectionReason.INVALID_CLASS_RECEIVED); } else { - log.warn("We close connection due RuleViolation.RULE_VIOLATION"); - shutDown(CloseConnectionReason.RULE_VIOLATION); + if (logReport) log.warn("We close connection due RuleViolation.RULE_VIOLATION"); + connection.shutDown(CloseConnectionReason.RULE_VIOLATION); } + resetReportedInvalidRequestsThrottle(logReport); return true; } else { + resetReportedInvalidRequestsThrottle(logReport); return false; } } + private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) { + if (logReport) { + if (unloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", unloggedInvalidRequestReports); + unloggedInvalidRequestReports = 0; + lastLoggedInvalidRequestReport = System.currentTimeMillis(); + } + } + private void handleException(Throwable e) { CloseConnectionReason closeConnectionReason; From a149d923926337f93004d4f7ebc0511e0557f1b8 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 12 Jul 2024 14:15:03 -0400 Subject: [PATCH 24/58] sleep after restarting seednode service --- seednode/haveno-seednode.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seednode/haveno-seednode.service b/seednode/haveno-seednode.service index 5bbae833..131e49e8 100644 --- a/seednode/haveno-seednode.service +++ b/seednode/haveno-seednode.service @@ -15,7 +15,7 @@ ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ --appName=haveno-XMR_STAGENET_Seed_2002 --xmrNode=[::1]:38088 -ExecStop=/bin/kill ${MAINPID} +ExecStop=/bin/kill ${MAINPID} ; sleep 5 Restart=always # Hardening From 23a7fb3d169ec86419d85fd8c3f4757dd5614a63 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 10 Jul 2024 11:01:25 -0400 Subject: [PATCH 25/58] cache wallet info last on poll --- .../haveno/core/xmr/wallet/XmrWalletService.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 682aafdb..11668929 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1845,9 +1845,6 @@ public class XmrWalletService { } } } - - // cache wallet info - cacheWalletInfo(); } catch (Exception e) { if (wallet == null || isShutDownStarted) return; boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); @@ -1857,6 +1854,15 @@ public class XmrWalletService { //e.printStackTrace(); } } finally { + + // cache wallet info last + if (wallet != null && !isShutDownStarted) { + try { + cacheWalletInfo(); + } catch (Exception e) { + e.printStackTrace(); + } + } pollInProgress = false; } } From 91e442d642278e82a93ffb740a9012cc9bf48bb3 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 16 Jul 2024 07:51:29 -0400 Subject: [PATCH 26/58] simplify the filter object message --- core/src/main/resources/i18n/displayStrings.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f8ef2894..f43f1a6d 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2142,7 +2142,7 @@ popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. \ A mandatory update was released which disables trading for old versions. \ Please check out the Haveno Forum for more information. -popup.warning.noFilter=We did not receive a filter object from the seed nodes. Please inform the Haveno network administrators to register a filter object with ctrl + f. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. Please inform the network administrators to register a filter object. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. \ Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. From 690f38e4dd8ba0ec996fa1f97f1e275dac182ac0 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 16 Jul 2024 12:30:30 -0400 Subject: [PATCH 27/58] bump version to 1.0.9 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/macosx/Info.plist | 4 ++-- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 703ada79..5a988f61 100644 --- a/build.gradle +++ b/build.gradle @@ -605,7 +605,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.8-SNAPSHOT' + version = '1.0.9-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 3554ea19..92ca9cf1 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.8"; + public static final String VERSION = "1.0.9"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index cf57e20e..1abf7683 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.8 + 1.0.9 CFBundleShortVersionString - 1.0.8 + 1.0.9 CFBundleExecutable Haveno diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index e4e47684..30f3f130 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.8"; + private static final String VERSION = "1.0.9"; private SeedNode seedNode; private Timer checkConnectionLossTime; From 40421eec75abd328aef132b9c4bceb83b36ae104 Mon Sep 17 00:00:00 2001 From: Milan Hauth Date: Sat, 13 Jul 2024 17:28:01 +0200 Subject: [PATCH 28/58] switch platform for script files --- build.gradle | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 5a988f61..054695ca 100644 --- a/build.gradle +++ b/build.gradle @@ -163,28 +163,33 @@ configure([project(':cli'), // edit generated shell scripts such that they expect to be executed in the // project root dir as opposed to a 'bin' subdirectory - def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat") - windowsScriptFile.text = windowsScriptFile.text.replace( - 'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%') - - def unixScriptFile = file("${rootProject.projectDir}/haveno-$applicationName") - unixScriptFile.text = unixScriptFile.text.replace( - 'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit') - - if (applicationName == 'desktop') { + if (osdetector.os == 'windows') { + def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat") windowsScriptFile.text = windowsScriptFile.text.replace( - 'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' + - '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + - '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + - '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + - '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED') + 'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%') + if (applicationName == 'desktop') { + windowsScriptFile.text = windowsScriptFile.text.replace( + 'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' + + '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + + '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + + '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED') + } + } + else { + def unixScriptFile = file("${rootProject.projectDir}/haveno-$applicationName") unixScriptFile.text = unixScriptFile.text.replace( - 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' + - '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + - '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + - '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + - '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"') + 'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit') + + if (applicationName == 'desktop') { + unixScriptFile.text = unixScriptFile.text.replace( + 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' + + '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + + '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + + '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"') + } } if (applicationName == 'apitest') { From 0a469db8f6b78076326e7a15c5720691fa7477fb Mon Sep 17 00:00:00 2001 From: fa2a5qj3 <174058787+fa2a5qj3@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:08:55 -0400 Subject: [PATCH 29/58] randomize completed trade info, fixes #1099 --- .../trade/statistics/TradeStatistics3.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java index 75db5880..37bb9d7b 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java @@ -54,6 +54,8 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.Random; import static com.google.common.base.Preconditions.checkNotNull; @@ -88,13 +90,32 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl Offer offer = checkNotNull(trade.getOffer()); return new TradeStatistics3(offer.getCurrencyCode(), trade.getPrice().getValue(), - trade.getAmount().longValueExact(), + fuzzTradeAmountReproducibly(trade), offer.getPaymentMethod().getId(), - trade.getTakeOfferDate().getTime(), + fuzzTradeDateReproducibly(trade), truncatedArbitratorNodeAddress, extraDataMap); } + private static long fuzzTradeAmountReproducibly(Trade trade) { // randomize completed trade info #1099 + long originalTimestamp = trade.getTakeOfferDate().getTime(); + long exactAmount = trade.getAmount().longValueExact(); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp + long adjustedAmount = (long) random.nextDouble( + exactAmount * 0.95, exactAmount * 1.05); + log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount); + return adjustedAmount; + } + + private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099 + long originalTimestamp = trade.getTakeOfferDate().getTime(); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp + long adjustedTimestamp = random.nextLong( + originalTimestamp-TimeUnit.HOURS.toMillis(24), originalTimestamp); + log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp)); + return adjustedTimestamp; + } + // This enum must not change the order as we use the ordinal for storage to reduce data size. // The payment method string can be quite long and would consume 15% more space. // When we get a new payment method we can add it to the enum at the end. Old users would add it as string if not From 9c359b5e29c567acd84c4def7a4d60b12e918342 Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 16 Jul 2024 16:11:50 -0400 Subject: [PATCH 30/58] support deleting payment accounts #1136 --- core/src/main/java/haveno/core/api/CoreApi.java | 4 ++++ .../core/api/CorePaymentAccountsService.java | 10 ++++++++++ .../daemon/grpc/GrpcPaymentAccountsService.java | 15 +++++++++++++++ proto/src/main/proto/grpc.proto | 9 +++++++++ 4 files changed, 38 insertions(+) diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index b4a293a3..fac92b31 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -496,6 +496,10 @@ public class CoreApi { tradeInstant); } + public void deletePaymentAccount(String paymentAccountId) { + paymentAccountsService.deletePaymentAccount(paymentAccountId); + } + public List getCryptoCurrencyPaymentMethods() { return paymentAccountsService.getCryptoCurrencyPaymentMethods(); } diff --git a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java index df88d094..dcf1ef0e 100644 --- a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java @@ -145,6 +145,16 @@ class CorePaymentAccountsService { return cryptoCurrencyAccount; } + synchronized void deletePaymentAccount(String paymentAccountId) { + accountService.checkAccountOpen(); + PaymentAccount paymentAccount = getPaymentAccount(paymentAccountId); + if (paymentAccount == null) throw new IllegalArgumentException(format("Payment account with id %s not found", paymentAccountId)); + user.removePaymentAccount(paymentAccount); + log.info("Deleted payment account with id {} and payment method {}.", + paymentAccount.getId(), + paymentAccount.getPaymentAccountPayload().getPaymentMethodId()); + } + // TODO Support all alt coin payment methods supported by UI. // The getCryptoCurrencyPaymentMethods method below will be // callable from the CLI when more are supported. diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java index e29b448f..ad1acce1 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java @@ -34,6 +34,8 @@ import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountReply; import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; import haveno.proto.grpc.CreatePaymentAccountReply; import haveno.proto.grpc.CreatePaymentAccountRequest; +import haveno.proto.grpc.DeletePaymentAccountReply; +import haveno.proto.grpc.DeletePaymentAccountRequest; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsReply; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; import haveno.proto.grpc.GetPaymentAccountFormReply; @@ -160,6 +162,19 @@ class GrpcPaymentAccountsService extends PaymentAccountsImplBase { } } + @Override + public void deletePaymentAccount(DeletePaymentAccountRequest req, + StreamObserver responseObserver) { + try { + coreApi.deletePaymentAccount(req.getPaymentAccountId()); + var reply = DeletePaymentAccountReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + @Override public void getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest req, StreamObserver responseObserver) { diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index c3dcf498..1bb4c9f5 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -586,6 +586,8 @@ service PaymentAccounts { } rpc CreateCryptoCurrencyPaymentAccount (CreateCryptoCurrencyPaymentAccountRequest) returns (CreateCryptoCurrencyPaymentAccountReply) { } + rpc DeletePaymentAccount (DeletePaymentAccountRequest) returns (DeletePaymentAccountReply) { + } rpc GetCryptoCurrencyPaymentMethods (GetCryptoCurrencyPaymentMethodsRequest) returns (GetCryptoCurrencyPaymentMethodsReply) { } rpc ValidateFormField (ValidateFormFieldRequest) returns (ValidateFormFieldReply) { @@ -639,6 +641,13 @@ message CreateCryptoCurrencyPaymentAccountRequest { bool trade_instant = 4; } +message DeletePaymentAccountRequest { + string payment_account_id = 1; +} + +message DeletePaymentAccountReply { +} + message CreateCryptoCurrencyPaymentAccountReply { PaymentAccount payment_account = 1; } From ff0ccc21e25c0b6e5a729f37c07c97295aa3095c Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 6 Jul 2024 08:45:28 -0400 Subject: [PATCH 31/58] fix error popup on delete outdated tor files --- .../p2p/peers/getdata/RequestDataHandler.java | 55 ++++++++++-------- .../peerexchange/PeerExchangeHandler.java | 58 ++++++++++--------- 2 files changed, 61 insertions(+), 52 deletions(-) diff --git a/p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java index 85a1cc89..f155c0e8 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java @@ -140,33 +140,38 @@ class RequestDataHandler implements MessageListener { getDataRequestType = getDataRequest.getClass().getSimpleName(); log.info("\n\n>> We send a {} to peer {}\n", getDataRequestType, nodeAddress); networkNode.addMessageListener(this); - SettableFuture future = networkNode.sendMessage(nodeAddress, getDataRequest); - //noinspection UnstableApiUsage - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(Connection connection) { - if (!stopped) { - log.trace("Send {} to {} succeeded.", getDataRequest, nodeAddress); - } else { - log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." + - "Might be caused by a previous timeout."); - } - } - @Override - public void onFailure(@NotNull Throwable throwable) { - if (!stopped) { - String errorMessage = "Sending getDataRequest to " + nodeAddress + - " failed. That is expected if the peer is offline.\n\t" + - "getDataRequest=" + getDataRequest + "." + - "\n\tException=" + throwable.getMessage(); - handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); - } else { - log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + - "Might be caused by a previous timeout."); + try { + SettableFuture future = networkNode.sendMessage(nodeAddress, getDataRequest); + //noinspection UnstableApiUsage + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + if (!stopped) { + log.trace("Send {} to {} succeeded.", getDataRequest, nodeAddress); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." + + "Might be caused by a previous timeout."); + } } - } - }, MoreExecutors.directExecutor()); + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending getDataRequest to " + nodeAddress + + " failed. That is expected if the peer is offline.\n\t" + + "getDataRequest=" + getDataRequest + "." + + "\n\tException=" + throwable.getMessage(); + handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + + "Might be caused by a previous timeout."); + } + } + }, MoreExecutors.directExecutor()); + } catch (Exception e) { + if (!networkNode.isShutDownStarted()) throw e; + } } else { log.warn("We have stopped already. We ignore that requestData call."); } diff --git a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java index cd12c17a..1a929289 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java @@ -115,35 +115,39 @@ class PeerExchangeHandler implements MessageListener { TIMEOUT, TimeUnit.SECONDS); } - SettableFuture future = networkNode.sendMessage(nodeAddress, getPeersRequest); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(Connection connection) { - if (!stopped) { - //TODO - /*if (!connection.getPeersNodeAddressOptional().isPresent()) { - connection.setPeersNodeAddress(nodeAddress); - log.warn("sendGetPeersRequest: !connection.getPeersNodeAddressOptional().isPresent()"); - }*/ - - PeerExchangeHandler.this.connection = connection; - connection.addMessageListener(PeerExchangeHandler.this); - } else { - log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onSuccess call."); + try { + SettableFuture future = networkNode.sendMessage(nodeAddress, getPeersRequest); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + if (!stopped) { + //TODO + /*if (!connection.getPeersNodeAddressOptional().isPresent()) { + connection.setPeersNodeAddress(nodeAddress); + log.warn("sendGetPeersRequest: !connection.getPeersNodeAddressOptional().isPresent()"); + }*/ + + PeerExchangeHandler.this.connection = connection; + connection.addMessageListener(PeerExchangeHandler.this); + } else { + log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onSuccess call."); + } } - } - - @Override - public void onFailure(@NotNull Throwable throwable) { - if (!stopped) { - String errorMessage = "Sending getPeersRequest to " + nodeAddress + - " failed. That is expected if the peer is offline. Exception=" + throwable.getMessage(); - handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, nodeAddress); - } else { - log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onFailure call."); + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending getPeersRequest to " + nodeAddress + + " failed. That is expected if the peer is offline. Exception=" + throwable.getMessage(); + handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, nodeAddress); + } else { + log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onFailure call."); + } } - } - }, MoreExecutors.directExecutor()); + }, MoreExecutors.directExecutor()); + } catch (Exception e) { + if (!networkNode.isShutDownStarted()) throw e; + } } else { log.debug("My node address is still null at sendGetPeersRequest. We ignore that call."); } From 96bcd7547ddf032d665846686a8d16d5c3476954 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 6 Jul 2024 08:46:45 -0400 Subject: [PATCH 32/58] ignore error syncing with progress on shut down --- core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 11668929..17883a36 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1465,7 +1465,7 @@ public class XmrWalletService { try { height = wallet.getHeight(); // can get read timeout while syncing } catch (Exception e) { - e.printStackTrace(); + if (!isShutDownStarted) e.printStackTrace(); return; } if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height); From 78a2476bb80f87201d331750ac34b1d15cabf837 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 6 Jul 2024 08:56:04 -0400 Subject: [PATCH 33/58] decrease startup timeout to 4 mins --- core/src/main/java/haveno/core/app/HavenoSetup.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 67214fa2..0d1ee2b9 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -113,7 +113,7 @@ import org.fxmisc.easybind.monadic.MonadicBinding; public class HavenoSetup { private static final String VERSION_FILE_NAME = "version"; - private static final long STARTUP_TIMEOUT_MINUTES = 5; + private static final long STARTUP_TIMEOUT_MINUTES = 4; private final DomainInitialisation domainInitialisation; private final P2PNetworkSetup p2PNetworkSetup; From 1c381de806eef2c880bc5078679cf0f0746f5831 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 13 Jul 2024 10:36:41 -0400 Subject: [PATCH 34/58] add mainnet monero nodes --- core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java index 894a71ce..643a5fae 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -86,11 +86,13 @@ public class XmrNodes { new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 18081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "xmr-node.cakewallet.com", 18081, 2, "@cakewallet"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "node.community.rino.io", 18081, 2, "@RINOwallet"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node3.monerodevs.org", 18089, 2, "@monerodevs.org"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "nodex.monerujo.io", 18081, 2, "@monerujo.io"), + new XmrNode(MoneroNodesOption.PROVIDED, null, null, "nodes.hashvault.pro", 18080, 2, "@HashVault"), + new XmrNode(MoneroNodesOption.PROVIDED, null, null, "p2pmd.xmrvsbeast.com", 18080, 2, "@xmrvsbeast"), + new XmrNode(MoneroNodesOption.PROVIDED, null, null, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), + new XmrNode(MoneroNodesOption.PROVIDED, null, null, "nodex.monerujo.io", 18081, 2, "@monerujo.io"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "rucknium.me", 18081, 2, "@Rucknium"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 18089, 2, "@sethforprivacy") + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 18089, 2, "@sethforprivacy"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node3.monerodevs.org", 18089, 2, "@monerodevs.org") ); default: throw new IllegalStateException("Unexpected base currency network: " + Config.baseCurrencyNetwork()); From 33bf54bcac6198394850e168415ad97626e7ea4f Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 16 Jul 2024 12:22:12 -0400 Subject: [PATCH 35/58] recover from payment received message not processed --- .../main/java/haveno/core/trade/Trade.java | 32 ++++++++++++------- .../core/trade/protocol/TradeProtocol.java | 14 ++++---- .../ProcessDepositsConfirmedMessage.java | 20 +++++++----- .../pendingtrades/PendingTradesViewModel.java | 2 ++ 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 6aa00373..a041d298 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -490,7 +490,6 @@ public abstract class Trade implements Tradable, Model { private Long payoutHeight; private IdlePayoutSyncer idlePayoutSyncer; @Getter - @Setter private boolean isCompleted; /////////////////////////////////////////////////////////////////////////////////////////// @@ -614,8 +613,8 @@ public abstract class Trade implements Tradable, Model { public void initialize(ProcessModelServiceProvider serviceProvider) { if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized"); - // done if payout unlocked - if (isPayoutUnlocked()) { + // done if payout unlocked and marked complete + if (isPayoutUnlocked() && isCompleted()) { clearAndShutDown(); return; } @@ -679,13 +678,12 @@ public abstract class Trade implements Tradable, Model { log.info("Payout published for {} {}", getClass().getSimpleName(), getId()); // sync main wallet to update pending balance - if (!isPayoutConfirmed()) { - new Thread(() -> { - HavenoUtils.waitFor(1000); - if (isShutDownStarted) return; - if (xmrConnectionService.isConnected()) syncAndPollWallet(); - }).start(); - } + ThreadUtils.submitToPool(() -> { + HavenoUtils.waitFor(1000); + if (isPayoutConfirmed()) return; + if (isShutDownStarted) return; + if (xmrConnectionService.isConnected()) syncAndPollWallet(); + }); // complete disputed trade if (getDisputeState().isArbitrated() && !getDisputeState().isClosed()) { @@ -707,7 +705,8 @@ public abstract class Trade implements Tradable, Model { if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) { if (!isInitialized) return; log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); - clearAndShutDown(); + if (isCompleted()) clearAndShutDown(); + else deleteWallet(); } }); }); @@ -790,6 +789,11 @@ public abstract class Trade implements Tradable, Model { return getArbitrator() == null ? null : getArbitrator().getNodeAddress(); } + public void setCompleted(boolean completed) { + this.isCompleted = completed; + if (isPayoutUnlocked()) clearAndShutDown(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // WALLET MANAGEMENT /////////////////////////////////////////////////////////////////////////////////////////// @@ -996,7 +1000,11 @@ public abstract class Trade implements Tradable, Model { private void forceCloseWallet() { if (wallet != null) { - xmrWalletService.forceCloseWallet(wallet, wallet.getPath()); + try { + xmrWalletService.forceCloseWallet(wallet, wallet.getPath()); + } catch (Exception e) { + log.warn("Error force closing wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + } stopPolling(); wallet = null; } diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 0ae5dba3..7b8b5e7d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -296,7 +296,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()"); + System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade) { @@ -333,7 +333,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() " + trade.getId()); + System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade) { @@ -376,7 +376,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() " + trade.getId()); + System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade) { @@ -422,7 +422,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleDepositResponse()"); + System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade) { @@ -452,7 +452,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handle(DepositsConfirmedMessage message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender); + System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { synchronized (trade) { @@ -481,7 +481,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage)"); + System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (!trade.isInitialized() || trade.isShutDown()) return; if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); @@ -535,7 +535,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)"); + System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java index 9673a565..a43c667d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -61,16 +61,20 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { } // update multisig hex - sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); + if (sender.getUpdatedMultisigHex() == null) { + sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - // try to import multisig hex (retry later) - ThreadUtils.submitToPool(() -> { - try { - trade.importMultisigHex(); - } catch (Exception e) { - e.printStackTrace(); + // try to import multisig hex (retry later) + if (!trade.isPayoutPublished()) { + ThreadUtils.submitToPool(() -> { + try { + trade.importMultisigHex(); + } catch (Exception e) { + e.printStackTrace(); + } + }); } - }); + } // persist processModel.getTradeManager().requestPersistence(); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index c8531387..41881c58 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -210,6 +210,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Wed, 17 Jul 2024 09:56:22 -0400 Subject: [PATCH 36/58] switch to next best monerod on various errors --- .../main/java/haveno/common/ThreadUtils.java | 1 + .../haveno/core/api/XmrConnectionService.java | 75 ++- .../haveno/core/offer/OpenOfferManager.java | 1 + .../tasks/MakerReserveOfferFunds.java | 1 + .../arbitration/ArbitrationManager.java | 1 + .../main/java/haveno/core/trade/Trade.java | 584 +++++++++++------- .../tasks/MaybeSendSignContractRequest.java | 1 + .../tasks/TakerReserveTradeFunds.java | 1 + .../main/java/haveno/core/xmr/Balances.java | 76 ++- .../core/xmr/wallet/XmrWalletService.java | 394 ++++++------ .../main/funds/withdrawal/WithdrawalView.java | 1 + 11 files changed, 677 insertions(+), 459 deletions(-) diff --git a/common/src/main/java/haveno/common/ThreadUtils.java b/common/src/main/java/haveno/common/ThreadUtils.java index 832ea91a..e463e831 100644 --- a/common/src/main/java/haveno/common/ThreadUtils.java +++ b/common/src/main/java/haveno/common/ThreadUtils.java @@ -47,6 +47,7 @@ public class ThreadUtils { synchronized (THREADS) { THREADS.put(threadId, Thread.currentThread()); } + Thread.currentThread().setName(threadId); command.run(); }); } diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 0c78fc98..f4122e4a 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -36,7 +36,10 @@ import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; @@ -103,6 +106,12 @@ public final class XmrConnectionService { private boolean isShutDownStarted; private List listeners = new ArrayList<>(); + // connection switching + private static final int EXCLUDE_CONNECTION_SECONDS = 300; + private static final int SKIP_SWITCH_WITHIN_MS = 60000; + private Set excludedConnections = new HashSet<>(); + private long lastSwitchRequestTimestamp; + @Inject public XmrConnectionService(P2PService p2PService, Config config, @@ -201,12 +210,6 @@ public final class XmrConnectionService { return connectionManager.getConnections(); } - public void switchToBestConnection() { - if (isFixedConnection() || !connectionManager.getAutoSwitch()) return; - MoneroRpcConnection bestConnection = getBestAvailableConnection(); - if (bestConnection != null) setConnection(bestConnection); - } - public void setConnection(String connectionUri) { accountService.checkAccountOpen(); connectionManager.setConnection(connectionUri); // listener will update connection list @@ -244,10 +247,67 @@ public final class XmrConnectionService { public MoneroRpcConnection getBestAvailableConnection() { accountService.checkAccountOpen(); List ignoredConnections = new ArrayList(); - if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); + addLocalNodeIfIgnored(ignoredConnections); return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0])); } + private MoneroRpcConnection getBestAvailableConnection(Collection ignoredConnections) { + accountService.checkAccountOpen(); + Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); + addLocalNodeIfIgnored(ignoredConnectionsSet); + return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); + } + + private void addLocalNodeIfIgnored(Collection ignoredConnections) { + if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); + } + + private void switchToBestConnection() { + if (isFixedConnection() || !connectionManager.getAutoSwitch()) { + log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled"); + return; + } + MoneroRpcConnection bestConnection = getBestAvailableConnection(); + if (bestConnection != null) setConnection(bestConnection); + } + + public boolean requestSwitchToNextBestConnection() { + log.warn("Request made to switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri()); + + // skip if connection is fixed + if (isFixedConnection() || !connectionManager.getAutoSwitch()) { + log.info("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled"); + return false; + } + + // skip if last switch was too recent + boolean skipSwitch = System.currentTimeMillis() - lastSwitchRequestTimestamp < SKIP_SWITCH_WITHIN_MS; + lastSwitchRequestTimestamp = System.currentTimeMillis(); + if (skipSwitch) { + log.warn("Skipping switch to next best Monero connection because last switch was less than {} seconds ago", SKIP_SWITCH_WITHIN_MS / 1000); + lastSwitchRequestTimestamp = System.currentTimeMillis(); + return false; + } + + // try to get connection to switch to + MoneroRpcConnection currentConnection = getConnection(); + if (currentConnection != null) excludedConnections.add(currentConnection); + MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections); + + // remove from excluded connections after period + UserThread.runAfter(() -> { + if (currentConnection != null) excludedConnections.remove(currentConnection); + }, EXCLUDE_CONNECTION_SECONDS); + + // switch to best connection + if (bestConnection == null) { + log.warn("Could not get connection to switch to"); + return false; + } + setConnection(bestConnection); + return true; + } + public void setAutoSwitch(boolean autoSwitch) { accountService.checkAccountOpen(); connectionManager.setAutoSwitch(autoSwitch); @@ -505,7 +565,6 @@ public final class XmrConnectionService { // register connection listener connectionManager.addListener(this::onConnectionChanged); - isInitialized = true; } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index e18d06ae..2be9a984 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1057,6 +1057,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } catch (Exception e) { log.warn("Error creating split output tx to fund offer {} at subaddress {}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 61cafd65..36759cc1 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -89,6 +89,7 @@ public class MakerReserveOfferFunds extends Task { log.warn("Error creating reserve tx, attempt={}/{}, offerId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; model.getProtocol().startTimeoutTimer(); // reset protocol timeout + if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 42b701d3..4e3c38ae 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -478,6 +478,7 @@ public final class ArbitrationManager extends DisputeManager { - ThreadUtils.submitToPool(() -> { // TODO: remove this? - ThreadUtils.execute(() -> onConnectionChanged(connection), getConnectionChangedThreadId()); - }); + ThreadUtils.execute(() -> onConnectionChanged(connection), getConnectionChangedThreadId()); }); // reset buyer's payment sent state if no ack receive @@ -847,6 +846,14 @@ public abstract class Trade implements Tradable, Model { } } + public boolean requestSwitchToNextBestConnection() { + if (xmrConnectionService.requestSwitchToNextBestConnection()) { + onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread + return true; + } + return false; + } + public boolean isIdling() { return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden } @@ -884,69 +891,8 @@ public abstract class Trade implements Tradable, Model { }).start(); } - public void importMultisigHex() { - synchronized (walletLock) { - synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - try { - doImportMultisigHex(); - break; - } catch (IllegalArgumentException e) { - throw e; - } catch (Exception e) { - log.warn("Failed to import multisig hex, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying - } - } - } - } - } - - private void doImportMultisigHex() { - - // ensure wallet sees deposits confirmed - if (!isDepositsConfirmed()) syncAndPollWallet(); - - // collect multisig hex from peers - List multisigHexes = new ArrayList(); - for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex()); - - // import multisig hex - log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size()); - long startTime = System.currentTimeMillis(); - if (!multisigHexes.isEmpty()) { - try { - wallet.importMultisigHex(multisigHexes.toArray(new String[0])); - } catch (MoneroError e) { - - // import multisig hex individually if one is invalid - if (isInvalidImportError(e.getMessage())) { - log.warn("Peer has invalid multisig hex for {} {}, importing individually", getClass().getSimpleName(), getShortId()); - boolean imported = false; - Exception lastError = null; - for (TradePeer peer : getOtherPeers()) { - if (peer.getUpdatedMultisigHex() == null) continue; - try { - wallet.importMultisigHex(peer.getUpdatedMultisigHex()); - imported = true; - } catch (MoneroError e2) { - lastError = e2; - if (isInvalidImportError(e2.getMessage())) { - log.warn("{} has invalid multisig hex for {} {}, error={}, multisigHex={}", getPeerRole(peer), getClass().getSimpleName(), getShortId(), e2.getMessage(), peer.getUpdatedMultisigHex()); - } else { - throw e2; - } - } - } - if (!imported) throw new IllegalArgumentException("Could not import any multisig hexes for " + getClass().getSimpleName() + " " + getShortId(), lastError); - } else { - throw e; - } - } - requestSaveWallet(); - } - log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); + private boolean isReadTimeoutError(String errMsg) { + return errMsg.contains("Read timed out"); } // TODO: checking error strings isn't robust, but the library doesn't provide a way to check if multisig hex is invalid. throw IllegalArgumentException from library on invalid multisig hex? @@ -962,7 +908,13 @@ public abstract class Trade implements Tradable, Model { } public void requestSaveWallet() { - ThreadUtils.submitToPool(() -> saveWallet()); // save wallet off main thread + + // save wallet off main thread + ThreadUtils.execute(() -> { + synchronized (walletLock) { + if (walletExists()) saveWallet(); + } + }, getId()); } public void saveWallet() { @@ -1109,6 +1061,104 @@ public abstract class Trade implements Tradable, Model { } } + public void importMultisigHex() { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + doImportMultisigHex(); + break; + } catch (IllegalArgumentException | IllegalStateException e) { + throw e; + } catch (Exception e) { + log.warn("Failed to import multisig hex, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); + if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } + } + } + } + + private void doImportMultisigHex() { + + // ensure wallet sees deposits confirmed + if (!isDepositsConfirmed()) syncAndPollWallet(); + + // collect multisig hex from peers + List multisigHexes = new ArrayList(); + for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex()); + + // import multisig hex + log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size()); + long startTime = System.currentTimeMillis(); + if (!multisigHexes.isEmpty()) { + try { + wallet.importMultisigHex(multisigHexes.toArray(new String[0])); + + // check if import is still needed // TODO: we once received a multisig hex which was too short, causing import to still be needed + if (wallet.isMultisigImportNeeded()) { + String errorMessage = "Multisig import still needed for " + getClass().getSimpleName() + " " + getShortId() + " after already importing, multisigHexes=" + multisigHexes; + log.warn(errorMessage); + + // ignore multisig hex which is significantly shorter than others + int maxLength = 0; + boolean removed = false; + for (String hex : multisigHexes) maxLength = Math.max(maxLength, hex.length()); + for (String hex : new ArrayList<>(multisigHexes)) { + if (hex.length() < maxLength / 2) { + String ignoringMessage = "Ignoring multisig hex from " + getMultisigHexRole(hex) + " for " + getClass().getSimpleName() + " " + getShortId() + " because it is too short, multisigHex=" + hex; + setErrorMessage(ignoringMessage); + log.warn(ignoringMessage); + multisigHexes.remove(hex); + removed = true; + } + } + + // re-import valid multisig hexes + if (removed) wallet.importMultisigHex(multisigHexes.toArray(new String[0])); + if (wallet.isMultisigImportNeeded()) throw new IllegalStateException(errorMessage); + } + } catch (MoneroError e) { + + // import multisig hex individually if one is invalid + if (isInvalidImportError(e.getMessage())) { + log.warn("Peer has invalid multisig hex for {} {}, importing individually", getClass().getSimpleName(), getShortId()); + boolean imported = false; + Exception lastError = null; + for (TradePeer peer : getOtherPeers()) { + if (peer.getUpdatedMultisigHex() == null) continue; + try { + wallet.importMultisigHex(peer.getUpdatedMultisigHex()); + imported = true; + } catch (MoneroError e2) { + lastError = e2; + if (isInvalidImportError(e2.getMessage())) { + log.warn("{} has invalid multisig hex for {} {}, error={}, multisigHex={}", getPeerRole(peer), getClass().getSimpleName(), getShortId(), e2.getMessage(), peer.getUpdatedMultisigHex()); + } else { + throw e2; + } + } + } + if (!imported) throw new IllegalArgumentException("Could not import any multisig hexes for " + getClass().getSimpleName() + " " + getShortId(), lastError); + } else { + throw e; + } + } + requestSaveWallet(); + } + log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); + } + + private String getMultisigHexRole(String multisigHex) { + if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator"; + if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer"; + if (multisigHex.equals(getSeller().getUpdatedMultisigHex())) return "seller"; + throw new IllegalArgumentException("Multisig hex does not belong to any peer"); + } + /** * Create the payout tx. * @@ -1125,9 +1175,12 @@ public abstract class Trade implements Tradable, Model { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { try { return doCreatePayoutTx(); + } catch (IllegalArgumentException | IllegalStateException e) { + throw e; } catch (Exception e) { log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } @@ -1139,14 +1192,10 @@ public abstract class Trade implements Tradable, Model { private MoneroTxWallet doCreatePayoutTx() { // check if multisig import needed - if (wallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed"); + if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId()); - // TODO: wallet sometimes returns empty data, after disconnect? - List txs = wallet.getTxs(); // TODO: this fetches from pool - if (txs.isEmpty()) { - log.warn("Restarting wallet for {} {} because deposit txs are missing to create payout tx", getClass().getSimpleName(), getId()); - forceRestartTradeWallet(); - } + // recover if missing wallet data + recoverIfMissingWalletData(); // gather info String sellerPayoutAddress = getSeller().getPayoutAddressString(); @@ -1184,11 +1233,15 @@ public abstract class Trade implements Tradable, Model { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { try { + if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId()); return createTx(txConfig); + } catch (IllegalArgumentException | IllegalStateException e) { + throw e; } catch (Exception e) { - if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee"); + if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee"); log.warn("Failed to create dispute payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } @@ -1205,23 +1258,30 @@ public abstract class Trade implements Tradable, Model { * @param publish publishes the signed payout tx if true */ public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { - log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); - - // TODO: wallet sometimes returns empty data, after disconnect? detect this condition with failure tolerance - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - try { - List txs = wallet.getTxs(); // TODO: this fetches from pool - if (txs.isEmpty()) { - log.warn("Restarting wallet for {} {} because deposit txs are missing to process payout tx", getClass().getSimpleName(), getId()); - forceRestartTradeWallet(); + synchronized (walletLock) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + doProcessPayoutTx(payoutTxHex, sign, publish); + break; + } catch (IllegalArgumentException | IllegalStateException e) { + throw e; + } catch (Exception e) { + log.warn("Failed to process payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } } - break; - } catch (Exception e) { - log.warn("Failed get wallet txs, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } + } + + private void doProcessPayoutTx(String payoutTxHex, boolean sign, boolean publish) { + log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); + + // recover if missing wallet data + recoverIfMissingWalletData(); // gather relevant info MoneroWallet wallet = getWallet(); @@ -1234,6 +1294,7 @@ public abstract class Trade implements Tradable, Model { MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); + if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if not signed // verify payout tx has exactly 2 destinations if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); @@ -1265,10 +1326,11 @@ public abstract class Trade implements Tradable, Model { if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); // check connection - if (sign || publish) verifyDaemonConnection(); + boolean doSign = sign && getPayoutTxHex() == null; + if (doSign || publish) verifyDaemonConnection(); // handle tx signing - if (sign) { + if (doSign) { // sign tx try { @@ -1283,6 +1345,7 @@ public abstract class Trade implements Tradable, Model { // describe result describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); payoutTx = describedTxSet.getTxs().get(0); + updatePayout(payoutTx); // verify fee is within tolerance by recreating payout tx // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? @@ -1294,22 +1357,16 @@ public abstract class Trade implements Tradable, Model { log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); } - // update trade state - updatePayout(payoutTx); + // save trade state requestPersistence(); // submit payout tx if (publish) { - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - try { - wallet.submitMultisigTxHex(payoutTxHex); - ThreadUtils.submitToPool(() -> pollWallet()); - break; - } catch (Exception e) { - log.warn("Failed to submit payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying - } + try { + wallet.submitMultisigTxHex(payoutTxHex); + setPayoutStatePublished(); + } catch (Exception e) { + throw new RuntimeException("Failed to submit payout tx for " + getClass().getSimpleName() + " " + getId(), e); } } } @@ -2244,10 +2301,6 @@ public abstract class Trade implements Tradable, Model { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private String getConnectionChangedThreadId() { - return getId() + ".onConnectionChanged"; - } - // lazy initialization private ObjectProperty getAmountProperty() { if (tradeAmountProperty == null) @@ -2263,6 +2316,10 @@ public abstract class Trade implements Tradable, Model { return tradeVolumeProperty; } + private String getConnectionChangedThreadId() { + return getId() + ".onConnectionChanged"; + } + private void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { @@ -2355,11 +2412,11 @@ public abstract class Trade implements Tradable, Model { } private void setPollPeriod(long pollPeriodMs) { - synchronized (walletLock) { + synchronized (pollLock) { if (this.isShutDownStarted) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; this.pollPeriodMs = pollPeriodMs; - if (isPollInProgress()) { + if (isPolling()) { stopPolling(); startPolling(); } @@ -2372,8 +2429,8 @@ public abstract class Trade implements Tradable, Model { } private void startPolling() { - synchronized (walletLock) { - if (isShutDownStarted || isPollInProgress()) return; + synchronized (pollLock) { + if (isShutDownStarted || isPolling()) return; updatePollPeriod(); log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); pollLooper = new TaskLooper(() -> pollWallet()); @@ -2382,153 +2439,156 @@ public abstract class Trade implements Tradable, Model { } private void stopPolling() { - synchronized (walletLock) { - if (isPollInProgress()) { + synchronized (pollLock) { + if (isPolling()) { pollLooper.stop(); pollLooper = null; } } } - private boolean isPollInProgress() { - synchronized (walletLock) { + private boolean isPolling() { + synchronized (pollLock) { return pollLooper != null; } } private void pollWallet() { - if (pollInProgress) return; + synchronized (pollLock) { + if (pollInProgress) return; + } doPollWallet(); } private void doPollWallet() { + if (isShutDownStarted) return; synchronized (pollLock) { pollInProgress = true; - try { + } + try { - // skip if payout unlocked - if (isPayoutUnlocked()) return; + // skip if payout unlocked + if (isPayoutUnlocked()) return; - // skip if deposit txs unknown or not requested - if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; + // skip if deposit txs unknown or not requested + if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; - // skip if daemon not synced - if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return; + // skip if daemon not synced + if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return; - // sync if wallet too far behind daemon - if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false); + // sync if wallet too far behind daemon + if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false); - // update deposit txs - if (!isDepositsUnlocked()) { + // update deposit txs + if (!isDepositsUnlocked()) { - // sync wallet if behind - syncWalletIfBehind(); + // sync wallet if behind + syncWalletIfBehind(); - // get txs from trade wallet - MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); - if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible - List txs; - if (!updatePool) txs = wallet.getTxs(query); - else { - synchronized (walletLock) { - synchronized (HavenoUtils.getDaemonLock()) { - txs = wallet.getTxs(query); - } - } - } - setDepositTxs(txs); - if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen - setStateDepositsSeen(); - - // set actual security deposits - if (getBuyer().getSecurityDeposit().longValueExact() == 0) { - BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); - BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); - getBuyer().setSecurityDeposit(buyerSecurityDeposit); - getSeller().setSecurityDeposit(sellerSecurityDeposit); - } - - // check for deposit txs confirmation - if (getMaker().getDepositTx().isConfirmed() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed(); - - // check for deposit txs unlocked - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { - setStateDepositsUnlocked(); - } - } - - // check for payout tx - if (isDepositsUnlocked()) { - - // determine if payout tx expected - boolean isPayoutExpected = isPaymentReceived() || hasPaymentReceivedMessage() || hasDisputeClosedMessage() || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal(); - - // sync wallet if payout expected or payout is published - if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind(); - - // rescan spent outputs to detect unconfirmed payout tx - if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { - try { - wallet.rescanSpent(); - } catch (Exception e) { - log.warn("Error rescanning spent outputs to detect payout tx for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); - } - } - - // get txs from trade wallet - MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - boolean updatePool = isPayoutExpected && !isPayoutConfirmed(); - if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible - List txs = null; - if (!updatePool) txs = wallet.getTxs(query); - else { - synchronized (walletLock) { - synchronized (HavenoUtils.getDaemonLock()) { - txs = wallet.getTxs(query); - } - } - } - setDepositTxs(txs); - - // check if any outputs spent (observed on payout published) - boolean hasSpentOutput = false; - boolean hasFailedTx = false; - for (MoneroTxWallet tx : txs) { - if (tx.isFailed()) hasFailedTx = true; - for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (Boolean.TRUE.equals(output.isSpent())) hasSpentOutput = true; - } - } - if (hasSpentOutput) setPayoutStatePublished(); - else if (hasFailedTx && isPayoutPublished()) { - log.warn("{} {} is in payout published state but has failed tx and no spent outputs, resetting payout state to unpublished", getClass().getSimpleName(), getShortId()); - setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); - } - - // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed) - for (MoneroTxWallet tx : txs) { - if (tx.isOutgoing() && !tx.isFailed()) { - updatePayout(tx); - setPayoutStatePublished(); - if (tx.isConfirmed()) setPayoutStateConfirmed(); - if (!tx.isLocked()) setPayoutStateUnlocked(); - } - } - } - } catch (Exception e) { - boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); - if (isConnectionRefused) forceRestartTradeWallet(); + // get txs from trade wallet + MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); + Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); + if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible + List txs; + if (!updatePool) txs = wallet.getTxs(query); else { - boolean isWalletConnected = isWalletConnectedToDaemon(); - if (!isShutDownStarted && wallet != null && isWalletConnected) { - log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); - //e.printStackTrace(); + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + txs = wallet.getTxs(query); + } } } - } finally { + setDepositTxs(txs); + if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen + setStateDepositsSeen(); + + // set actual security deposits + if (getBuyer().getSecurityDeposit().longValueExact() == 0) { + BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); + getBuyer().setSecurityDeposit(buyerSecurityDeposit); + getSeller().setSecurityDeposit(sellerSecurityDeposit); + } + + // check for deposit txs confirmation + if (getMaker().getDepositTx().isConfirmed() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed(); + + // check for deposit txs unlocked + if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { + setStateDepositsUnlocked(); + } + } + + // check for payout tx + if (isDepositsUnlocked()) { + + // determine if payout tx expected + boolean isPayoutExpected = isPaymentReceived() || hasPaymentReceivedMessage() || hasDisputeClosedMessage() || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal(); + + // sync wallet if payout expected or payout is published + if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind(); + + // rescan spent outputs to detect unconfirmed payout tx + if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { + wallet.rescanSpent(); + } + + // get txs from trade wallet + MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); + boolean updatePool = isPayoutExpected && !isPayoutConfirmed(); + if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible + List txs = null; + if (!updatePool) txs = wallet.getTxs(query); + else { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + txs = wallet.getTxs(query); + } + } + } + setDepositTxs(txs); + + // check if any outputs spent (observed on payout published) + boolean hasSpentOutput = false; + boolean hasFailedTx = false; + for (MoneroTxWallet tx : txs) { + if (tx.isFailed()) hasFailedTx = true; + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (Boolean.TRUE.equals(output.isSpent())) hasSpentOutput = true; + } + } + if (hasSpentOutput) setPayoutStatePublished(); + else if (hasFailedTx && isPayoutPublished()) { + log.warn("{} {} is in payout published state but has failed tx and no spent outputs, resetting payout state to unpublished", getClass().getSimpleName(), getShortId()); + setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); + } + + // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed) + for (MoneroTxWallet tx : txs) { + if (tx.isOutgoing() && !tx.isFailed()) { + updatePayout(tx); + setPayoutStatePublished(); + if (tx.isConfirmed()) setPayoutStateConfirmed(); + if (!tx.isLocked()) setPayoutStateUnlocked(); + } + } + } + } catch (Exception e) { + boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); + if (isConnectionRefused) forceRestartTradeWallet(); + else { + boolean isWalletConnected = isWalletConnectedToDaemon(); + if (!isShutDownStarted && wallet != null && isWalletConnected) { + log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); + requestSwitchToNextBestConnection(); + //e.printStackTrace(); + } + } + } finally { + synchronized (pollLock) { pollInProgress = false; } + requestSaveWallet(); } } @@ -2553,6 +2613,70 @@ public abstract class Trade implements Tradable, Model { depositTxsUpdateCounter.set(depositTxsUpdateCounter.get() + 1); } + // TODO: wallet is sometimes missing balance or deposits, due to specific daemon connections, not saving? + private void recoverIfMissingWalletData() { + synchronized (walletLock) { + if (isWalletMissingData()) { + log.warn("Wallet is missing data for {} {}, attempting to recover", getClass().getSimpleName(), getShortId()); + + // force restart wallet + forceRestartTradeWallet(); + + // rescan blockchain with global daemon lock + synchronized (HavenoUtils.getDaemonLock()) { + Long timeout = null; + try { + + // extend rpc timeout for rescan + if (wallet instanceof MoneroWalletRpc) { + timeout = ((MoneroWalletRpc) wallet).getRpcConnection().getTimeout(); + ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(EXTENDED_RPC_TIMEOUT); + } + + // rescan blockchain + log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); + wallet.rescanBlockchain(); + } catch (Exception e) { + if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while + throw e; + } finally { + + // restore rpc timeout + if (wallet instanceof MoneroWalletRpc) { + ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(timeout); + } + } + } + + // import multisig hex + log.warn("Importing multisig hex to recover wallet data for {} {}", getClass().getSimpleName(), getShortId()); + importMultisigHex(); + } + } + + // check again after releasing lock + if (isWalletMissingData()) throw new IllegalStateException("Wallet is still missing data after attempting recovery for " + getClass().getSimpleName() + " " + getShortId()); + } + + private boolean isWalletMissingData() { + synchronized (walletLock) { + if (!isDepositsUnlocked() || isPayoutPublished()) return false; + if (getMakerDepositTx() == null) { + log.warn("Missing maker deposit tx for {} {}", getClass().getSimpleName(), getId()); + return true; + } + if (getTakerDepositTx() == null) { + log.warn("Missing taker deposit tx for {} {}", getClass().getSimpleName(), getId()); + return true; + } + if (wallet.getBalance().equals(BigInteger.ZERO)) { + log.warn("Wallet balance is zero for {} {}", getClass().getSimpleName(), getId()); + return true; + } + return false; + } + } + private void forceRestartTradeWallet() { if (isShutDownStarted || restartInProgress) return; log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId()); @@ -2560,7 +2684,7 @@ public abstract class Trade implements Tradable, Model { forceCloseWallet(); if (!isShutDownStarted) wallet = getWallet(); restartInProgress = false; - doPollWallet(); + pollWallet(); if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId()); } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 353c167a..bcc75132 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -105,6 +105,7 @@ public class MaybeSendSignContractRequest extends TradeTask { } catch (Exception e) { log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index 6e0719c1..0f0dd1cb 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -71,6 +71,7 @@ public class TakerReserveTradeFunds extends TradeTask { } catch (Exception e) { log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } diff --git a/core/src/main/java/haveno/core/xmr/Balances.java b/core/src/main/java/haveno/core/xmr/Balances.java index 45525e76..cfae1cb5 100644 --- a/core/src/main/java/haveno/core/xmr/Balances.java +++ b/core/src/main/java/haveno/core/xmr/Balances.java @@ -35,6 +35,8 @@ package haveno.core.xmr; import com.google.inject.Inject; + +import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.core.api.model.XmrBalanceInfo; import haveno.core.offer.OpenOffer; @@ -103,7 +105,7 @@ public class Balances { updateBalances(); } }); - updateBalances(); + doUpdateBalances(); } public XmrBalanceInfo getBalances() { @@ -117,42 +119,48 @@ public class Balances { } private void updateBalances() { + ThreadUtils.submitToPool(() -> doUpdateBalances()); + } + + private void doUpdateBalances() { synchronized (this) { - - // get wallet balances - BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance(); - availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance(); + synchronized (XmrWalletService.WALLET_LOCK) { - // calculate pending balance by adding frozen trade balances - reserved amounts - pendingBalance = balance.subtract(availableBalance); - List trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList()); - for (Trade trade : trades) { - if (trade.getFrozenAmount().equals(new BigInteger("0"))) continue; - BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); - pendingBalance = pendingBalance.add(trade.getFrozenAmount()).subtract(trade.getReservedAmount()).subtract(tradeFee).subtract(trade.getSelf().getDepositTxFee()); + // get wallet balances + BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance(); + availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance(); + + // calculate pending balance by adding frozen trade balances - reserved amounts + pendingBalance = balance.subtract(availableBalance); + List trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList()); + for (Trade trade : trades) { + if (trade.getFrozenAmount().equals(new BigInteger("0"))) continue; + BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); + pendingBalance = pendingBalance.add(trade.getFrozenAmount()).subtract(trade.getReservedAmount()).subtract(tradeFee).subtract(trade.getSelf().getDepositTxFee()); + } + + // calculate reserved offer balance + reservedOfferBalance = BigInteger.ZERO; + if (xmrWalletService.getWallet() != null) { + List frozenOutputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); + for (MoneroOutputWallet frozenOutput : frozenOutputs) reservedOfferBalance = reservedOfferBalance.add(frozenOutput.getAmount()); + } + for (Trade trade : trades) { + reservedOfferBalance = reservedOfferBalance.subtract(trade.getFrozenAmount()); // subtract frozen trade balances + } + + // calculate reserved trade balance + reservedTradeBalance = BigInteger.ZERO; + for (Trade trade : trades) { + reservedTradeBalance = reservedTradeBalance.add(trade.getReservedAmount()); + } + + // calculate reserved balance + reservedBalance = reservedOfferBalance.add(reservedTradeBalance); + + // notify balance update + UserThread.execute(() -> updateCounter.set(updateCounter.get() + 1)); } - - // calculate reserved offer balance - reservedOfferBalance = BigInteger.ZERO; - if (xmrWalletService.getWallet() != null) { - List frozenOutputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); - for (MoneroOutputWallet frozenOutput : frozenOutputs) reservedOfferBalance = reservedOfferBalance.add(frozenOutput.getAmount()); - } - for (Trade trade : trades) { - reservedOfferBalance = reservedOfferBalance.subtract(trade.getFrozenAmount()); // subtract frozen trade balances - } - - // calculate reserved trade balance - reservedTradeBalance = BigInteger.ZERO; - for (Trade trade : trades) { - reservedTradeBalance = reservedTradeBalance.add(trade.getReservedAmount()); - } - - // calculate reserved balance - reservedBalance = reservedOfferBalance.add(reservedTradeBalance); - - // notify balance update - UserThread.execute(() -> updateCounter.set(updateCounter.get() + 1)); } } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 17883a36..d135526a 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -24,6 +24,7 @@ import com.google.inject.name.Named; import common.utils.JsonUtils; import haveno.common.ThreadUtils; +import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.FileUtil; @@ -67,6 +68,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.beans.property.LongProperty; @@ -155,14 +157,16 @@ public class XmrWalletService { private TradeManager tradeManager; private MoneroWallet wallet; public static final Object WALLET_LOCK = new Object(); - private boolean wasWalletSynced = false; + private boolean wasWalletSynced; private final Map> txCache = new HashMap>(); - private boolean isClosingWallet = false; - private boolean isShutDownStarted = false; + private boolean isClosingWallet; + private boolean isShutDownStarted; private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type - private Long syncStartHeight = null; - private TaskLooper syncWithProgressLooper = null; - CountDownLatch syncWithProgressLatch; + private Long syncStartHeight; + private TaskLooper syncProgressLooper; + private CountDownLatch syncProgressLatch; + private Timer syncProgressTimeout; + private static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 45; // wallet polling and cache private TaskLooper pollLooper; @@ -933,7 +937,7 @@ public class XmrWalletService { e.printStackTrace(); // force close wallet - forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); + forceCloseMainWallet(); } log.info("Done shutting down {}", getClass().getSimpleName()); @@ -1281,22 +1285,7 @@ public class XmrWalletService { else log.info(appliedMsg); // listen for connection changes - xmrConnectionService.addConnectionListener(connection -> { - - // force restart main wallet if connection changed before synced - if (!wasWalletSynced) { - if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return; - ThreadUtils.submitToPool(() -> { - log.warn("Force restarting main wallet because connection changed before inital sync"); - forceRestartMainWallet(); - }); - return; - } else { - - // apply connection changes - ThreadUtils.execute(() -> onConnectionChanged(connection), THREAD_ID); - } - }); + xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> onConnectionChanged(connection), THREAD_ID)); // initialize main wallet when daemon synced walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected(); @@ -1305,111 +1294,110 @@ public class XmrWalletService { } private void initMainWalletIfConnected() { - ThreadUtils.execute(() -> { - synchronized (WALLET_LOCK) { - if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) { - maybeInitMainWallet(true); - if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); - } - } - }, THREAD_ID); + if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) { + maybeInitMainWallet(true); + } } private void maybeInitMainWallet(boolean sync) { - try { - maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); - } catch (Exception e) { - log.warn("Error initializing main wallet: " + e.getMessage()); - e.printStackTrace(); - HavenoUtils.havenoSetup.getTopErrorMsg().set(e.getMessage()); - throw e; - } + maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); } private void maybeInitMainWallet(boolean sync, int numAttempts) { - synchronized (WALLET_LOCK) { - if (isShutDownStarted) return; - - // open or create wallet main wallet - if (wallet == null) { - MoneroDaemonRpc daemon = xmrConnectionService.getDaemon(); - log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri())); - if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { - wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced)); - } else if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { - wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort); - - // set wallet creation date to yesterday to guarantee complete restore - LocalDateTime localDateTime = LocalDate.now().atStartOfDay().minusDays(1); - long date = localDateTime.toEpochSecond(ZoneOffset.UTC); - user.setWalletCreationDate(date); + ThreadUtils.execute(() -> { + synchronized (WALLET_LOCK) { + if (isShutDownStarted) return; + + // open or create wallet main wallet + if (wallet == null) { + MoneroDaemonRpc daemon = xmrConnectionService.getDaemon(); + log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri())); + if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { + wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced)); + } else if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { + wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort); + + // set wallet creation date to yesterday to guarantee complete restore + LocalDateTime localDateTime = LocalDate.now().atStartOfDay().minusDays(1); + long date = localDateTime.toEpochSecond(ZoneOffset.UTC); + user.setWalletCreationDate(date); + } + isClosingWallet = false; } - isClosingWallet = false; - } + + // sync wallet and register listener + if (wallet != null && !isShutDownStarted) { + log.info("Monero wallet path={}", wallet.getPath()); + + // sync main wallet if applicable + if (sync && numAttempts > 0) { + try { - // sync wallet and register listener - if (wallet != null && !isShutDownStarted) { - log.info("Monero wallet path={}", wallet.getPath()); + // switch connection if disconnected + if (!wallet.isConnectedToDaemon()) { + log.warn("Switching connection before syncing with progress because disconnected"); + if (requestSwitchToNextBestConnection()) return; // calls back to this method + } + + // sync main wallet + log.info("Syncing main wallet"); + long time = System.currentTimeMillis(); + syncWithProgress(); // blocking + log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); - // sync main wallet if applicable - if (sync && numAttempts > 0) { - try { - - // sync main wallet - log.info("Syncing main wallet"); - long time = System.currentTimeMillis(); - syncWithProgress(); // blocking - log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); - doPollWallet(true); - - // log wallet balances - if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { - BigInteger balance = getBalance(); - BigInteger unlockedBalance = getAvailableBalance(); - log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); - } - - // reapply connection after wallet synced - onConnectionChanged(xmrConnectionService.getConnection()); - - // reset internal state if main wallet was swapped - resetIfWalletChanged(); - - // signal that main wallet is synced - doneDownload(); - - // notify setup that main wallet is initialized - // TODO: app fully initializes after this is set to true, even though wallet might not be initialized if unconnected. wallet will be created when connection detected - // refactor startup to call this and sync off main thread? but the calls to e.g. getBalance() fail with 'wallet and network is not yet initialized' - HavenoUtils.havenoSetup.getWalletInitialized().set(true); - - // save but skip backup on initialization - saveMainWallet(false); - } catch (Exception e) { - if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized - log.warn("Error initially syncing main wallet: {}", e.getMessage()); - if (numAttempts <= 1) { - log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts); + // poll wallet + doPollWallet(true); + if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); + + // log wallet balances + if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { + BigInteger balance = getBalance(); + BigInteger unlockedBalance = getAvailableBalance(); + log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); + } + + // reapply connection after wallet synced (might reinitialize wallet on new thread) + ThreadUtils.execute(() -> onConnectionChanged(xmrConnectionService.getConnection()), THREAD_ID); + + // reset internal state if main wallet was swapped + resetIfWalletChanged(); + + // signal that main wallet is synced + doneDownload(); + + // notify setup that main wallet is initialized + // TODO: app fully initializes after this is set to true, even though wallet might not be initialized if unconnected. wallet will be created when connection detected + // refactor startup to call this and sync off main thread? but the calls to e.g. getBalance() fail with 'wallet and network is not yet initialized' HavenoUtils.havenoSetup.getWalletInitialized().set(true); + + // save but skip backup on initialization saveMainWallet(false); - - // reschedule to init main wallet - UserThread.runAfter(() -> { - ThreadUtils.execute(() -> maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS), THREAD_ID); - }, xmrConnectionService.getRefreshPeriodMs() / 1000); - } else { - log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); - UserThread.runAfter(() -> { - ThreadUtils.execute(() -> maybeInitMainWallet(true, numAttempts - 1), THREAD_ID); - }, xmrConnectionService.getRefreshPeriodMs() / 1000); + } catch (Exception e) { + if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized + log.warn("Error initially syncing main wallet: {}", e.getMessage()); + if (numAttempts <= 1) { + log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts); + HavenoUtils.havenoSetup.getWalletInitialized().set(true); + saveMainWallet(false); + + // reschedule to init main wallet + UserThread.runAfter(() -> { + maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); + }, xmrConnectionService.getRefreshPeriodMs() / 1000); + } else { + log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); + UserThread.runAfter(() -> { + maybeInitMainWallet(true, numAttempts - 1); + }, xmrConnectionService.getRefreshPeriodMs() / 1000); + } } } + + // start polling main wallet + startPolling(); } - - // start polling main wallet - startPolling(); } - } + }, THREAD_ID); } private void resetIfWalletChanged() { @@ -1431,6 +1419,9 @@ public class XmrWalletService { private void syncWithProgress() { + // start sync progress timeout + resetSyncProgressTimeout(); + // show sync progress updateSyncProgress(wallet.getHeight()); @@ -1458,8 +1449,8 @@ public class XmrWalletService { // poll wallet for progress wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); - syncWithProgressLatch = new CountDownLatch(1); - syncWithProgressLooper = new TaskLooper(() -> { + syncProgressLatch = new CountDownLatch(1); + syncProgressLooper = new TaskLooper(() -> { if (wallet == null) return; long height = 0; try { @@ -1470,29 +1461,22 @@ public class XmrWalletService { } if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height); else { - syncWithProgressLooper.stop(); + syncProgressLooper.stop(); wasWalletSynced = true; updateSyncProgress(height); - syncWithProgressLatch.countDown(); + syncProgressLatch.countDown(); } }); - syncWithProgressLooper.start(1000); - HavenoUtils.awaitLatch(syncWithProgressLatch); + syncProgressLooper.start(1000); + HavenoUtils.awaitLatch(syncProgressLatch); wallet.stopSyncing(); if (!wasWalletSynced) throw new IllegalStateException("Failed to sync wallet with progress"); } - private void stopSyncWithProgress() { - if (syncWithProgressLooper != null) { - syncWithProgressLooper.stop(); - syncWithProgressLooper = null; - syncWithProgressLatch.countDown(); - } - } - private void updateSyncProgress(long height) { UserThread.execute(() -> { walletHeight.set(height); + resetSyncProgressTimeout(); // new wallet reports height 1 before synced if (height == 1) { @@ -1509,6 +1493,18 @@ public class XmrWalletService { }); } + private synchronized void resetSyncProgressTimeout() { + if (syncProgressTimeout != null) syncProgressTimeout.stop(); + syncProgressTimeout = UserThread.runAfter(() -> { + if (isShutDownStarted || wasWalletSynced) return; + log.warn("Sync progress timeout called"); + forceCloseMainWallet(); + requestSwitchToNextBestConnection(); + maybeInitMainWallet(true); + resetSyncProgressTimeout(); + }, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + private MoneroWalletFull createWalletFull(MoneroWalletConfig config) { // must be connected to daemon @@ -1545,7 +1541,7 @@ public class XmrWalletService { // open wallet config.setNetworkType(getMoneroNetworkType()); config.setServer(connection); - log.info("Opening full wallet " + config.getPath() + " with monerod=" + connection.getUri()); + log.info("Opening full wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri()); walletFull = MoneroWalletFull.openWallet(config); if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done opening full wallet " + config.getPath()); @@ -1605,7 +1601,7 @@ public class XmrWalletService { if (!applyProxyUri) connection.setProxyUri(null); // open wallet - log.info("Opening RPC wallet " + config.getPath() + " with monerod=" + connection.getUri()); + log.info("Opening RPC wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri()); config.setServer(connection); walletRpc.openWallet(config); if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); @@ -1667,20 +1663,37 @@ public class XmrWalletService { String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String newProxyUri = connection == null ? null : connection.getProxyUri(); log.info("Setting daemon connection for main wallet: uri={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri); + + // force restart main wallet if connection changed before synced + if (!wasWalletSynced) { + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return; + log.warn("Force restarting main wallet because connection changed before inital sync"); + forceRestartMainWallet(); + return; + } + + // update connection if (wallet instanceof MoneroWalletRpc) { if (StringUtils.equals(oldProxyUri, newProxyUri)) { wallet.setDaemonConnection(connection); } else { - log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); + log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet closeMainWallet(true); maybeInitMainWallet(false); + return; // wallet is re-initialized } } else { wallet.setDaemonConnection(connection); wallet.setProxyUri(connection.getProxyUri()); } - // sync wallet on new thread + // switch if wallet disconnected + if (Boolean.TRUE.equals(connection.isConnected() && !wallet.isConnectedToDaemon())) { + log.warn("Switching to next best connection because main wallet is disconnected"); + if (requestSwitchToNextBestConnection()) return; // calls back to this method + } + + // update poll period if (connection != null && !isShutDownStarted) { wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); updatePollPeriod(); @@ -1735,25 +1748,21 @@ public class XmrWalletService { } private void forceCloseMainWallet() { + stopPolling(); isClosingWallet = true; forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); - stopPolling(); - stopSyncWithProgress(); wallet = null; } private void forceRestartMainWallet() { log.warn("Force restarting main wallet"); forceCloseMainWallet(); - synchronized (WALLET_LOCK) { - maybeInitMainWallet(true); - } + maybeInitMainWallet(true); } private void startPolling() { synchronized (WALLET_LOCK) { - if (isShutDownStarted || isPollInProgress()) return; - log.info("Starting to poll main wallet"); + if (isShutDownStarted || isPolling()) return; updatePollPeriod(); pollLooper = new TaskLooper(() -> pollWallet()); pollLooper.start(pollPeriodMs); @@ -1761,13 +1770,13 @@ public class XmrWalletService { } private void stopPolling() { - if (isPollInProgress()) { + if (isPolling()) { pollLooper.stop(); pollLooper = null; } } - private boolean isPollInProgress() { + private boolean isPolling() { return pollLooper != null; } @@ -1785,7 +1794,7 @@ public class XmrWalletService { if (this.isShutDownStarted) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; this.pollPeriodMs = pollPeriodMs; - if (isPollInProgress()) { + if (isPolling()) { stopPolling(); startPolling(); } @@ -1793,69 +1802,73 @@ public class XmrWalletService { } private void pollWallet() { - if (pollInProgress) return; + synchronized (pollLock) { + if (pollInProgress) return; + } doPollWallet(true); } private void doPollWallet(boolean updateTxs) { synchronized (pollLock) { - if (isShutDownStarted) return; pollInProgress = true; - try { + } + if (isShutDownStarted) return; + try { - // skip if daemon not synced - MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); - if (lastInfo == null) { - log.warn("Last daemon info is null"); - return; - } - if (!xmrConnectionService.isSyncedWithinTolerance()) { - log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight()); - return; - } + // skip if daemon not synced + MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); + if (lastInfo == null) { + log.warn("Last daemon info is null"); + return; + } + if (!xmrConnectionService.isSyncedWithinTolerance()) { + log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight()); + return; + } - // switch to best connection if wallet is too far behind - if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) { - log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight()); - xmrConnectionService.switchToBestConnection(); - } + // switch to best connection if wallet is too far behind + if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) { + log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight()); + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); + } - // sync wallet if behind daemon - if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { - synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations - syncMainWallet(); - } + // sync wallet if behind daemon + if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { + synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations + syncMainWallet(); } + } - // fetch transactions from pool and store to cache - // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? - if (updateTxs) { - synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations - synchronized (HavenoUtils.getDaemonLock()) { - try { - cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); - lastPollSuccessTimestamp = System.currentTimeMillis(); - } catch (Exception e) { // fetch from pool can fail - if (!isShutDownStarted) { - if (lastPollSuccessTimestamp == null || System.currentTimeMillis() - lastPollSuccessTimestamp > LOG_POLL_ERROR_AFTER_MS) { // only log if not recently successful - log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); - } + // fetch transactions from pool and store to cache + // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? + if (updateTxs) { + synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations + synchronized (HavenoUtils.getDaemonLock()) { + try { + cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); + lastPollSuccessTimestamp = System.currentTimeMillis(); + } catch (Exception e) { // fetch from pool can fail + if (!isShutDownStarted) { + if (lastPollSuccessTimestamp == null || System.currentTimeMillis() - lastPollSuccessTimestamp > LOG_POLL_ERROR_AFTER_MS) { // only log if not recently successful + log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); } } } } } - } catch (Exception e) { - if (wallet == null || isShutDownStarted) return; - boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); - if (isConnectionRefused) forceRestartMainWallet(); - else if (isWalletConnectedToDaemon()) { - log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); - //e.printStackTrace(); - } - } finally { + } + } catch (Exception e) { + if (wallet == null || isShutDownStarted) return; + boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); + if (isConnectionRefused) forceRestartMainWallet(); + else if (isWalletConnectedToDaemon()) { + log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); + //e.printStackTrace(); + } + } finally { - // cache wallet info last + // cache wallet info last + synchronized (WALLET_LOCK) { if (wallet != null && !isShutDownStarted) { try { cacheWalletInfo(); @@ -1863,6 +1876,9 @@ public class XmrWalletService { e.printStackTrace(); } } + } + + synchronized (pollLock) { pollInProgress = false; } } @@ -1887,6 +1903,10 @@ public class XmrWalletService { } } + public boolean requestSwitchToNextBestConnection() { + return xmrConnectionService.requestSwitchToNextBestConnection(); + } + private void onNewBlock(long height) { UserThread.execute(() -> { walletHeight.set(height); 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 44a66881..cab5d715 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 @@ -270,6 +270,7 @@ public class WithdrawalView extends ActivatableView { if (isNotEnoughMoney(e.getMessage())) throw e; log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } From d69dcae875bb711e87aa725f4f4624ce79909e41 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 12 Jul 2024 14:05:07 -0400 Subject: [PATCH 37/58] preserve offers unless invalid #1115 --- .../java/haveno/core/offer/OpenOffer.java | 12 +- .../haveno/core/offer/OpenOfferManager.java | 125 ++++++++++-------- .../tasks/MakerReserveOfferFunds.java | 9 +- .../tasks/MakerSendSignOfferRequest.java | 13 +- .../portfolio/openoffer/OpenOffersView.java | 4 +- proto/src/main/proto/pb.proto | 2 +- 6 files changed, 90 insertions(+), 75 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index dcb28259..b0df3e0e 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -53,7 +53,7 @@ import java.util.Optional; public final class OpenOffer implements Tradable { public enum State { - SCHEDULED, + PENDING, AVAILABLE, RESERVED, CLOSED, @@ -120,7 +120,7 @@ public final class OpenOffer implements Tradable { this.offer = offer; this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; - state = State.SCHEDULED; + state = State.PENDING; } public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) { @@ -165,8 +165,8 @@ public final class OpenOffer implements Tradable { this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; - if (this.state == State.RESERVED) - setState(State.AVAILABLE); + // reset reserved state to available + if (this.state == State.RESERVED) setState(State.AVAILABLE); } @Override @@ -232,8 +232,8 @@ public final class OpenOffer implements Tradable { return stateProperty; } - public boolean isScheduled() { - return state == State.SCHEDULED; + public boolean isPending() { + return state == State.PENDING; } public boolean isAvailable() { diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 2be9a984..58581118 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -132,7 +132,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30; private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30); private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2; - private static final int MAX_PROCESS_ATTEMPTS = 5; + private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process pending offer only on republish cycle after this many attempts private final CoreContext coreContext; private final KeyRing keyRing; @@ -252,17 +252,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // read open offers persistenceManager.readPersisted(persisted -> { - openOffers.setAll(persisted.getList()); - openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); + openOffers.setAll(persisted.getList()); + openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); - // read signed offers - signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { - signedOffers.setAll(signedOfferPersisted.getList()); - completeHandler.run(); - }, - completeHandler); - }, - completeHandler); + // read signed offers + signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { + signedOffers.setAll(signedOfferPersisted.getList()); + completeHandler.run(); + }, + completeHandler); + }, + completeHandler); } private synchronized void maybeInitializeKeyImagePoller() { @@ -472,17 +472,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); - // process scheduled offers - processScheduledOffers((transaction) -> {}, (errorMessage) -> { - log.warn("Error processing unposted offers: " + errorMessage); - }); + // process pending offers + processPendingOffers(false); - // register to process unposted offers on new block + // register to process pending offers on new block xmrWalletService.addWalletListener(new MoneroWalletListener() { @Override public void onNewBlock(long height) { - processScheduledOffers((transaction) -> {}, (errorMessage) -> { - log.warn("Error processing unposted offers on new block {}: {}", height, errorMessage); + + // process pending offers on new block a few times + processPendingOffers(true, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing pending offers on new block {}: {}", height, errorMessage); }); } }); @@ -549,16 +549,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); addOpenOffer(openOffer); - processUnpostedOffer(getOpenOffers(), openOffer, (transaction) -> { + processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); resultHandler.handleResult(transaction); }, (errorMessage) -> { if (openOffer.isCanceled()) latch.countDown(); else { - log.warn("Error processing unposted offer {}: {}", openOffer.getId(), errorMessage); + log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage); doCancelOffer(openOffer); - offer.setErrorMessage(errorMessage); latch.countDown(); errorMessageHandler.handleErrorMessage(errorMessage); } @@ -583,9 +582,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe public void activateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - if (openOffer.isScheduled()) { - resultHandler.handleResult(); // ignore if scheduled - } else if (!offersToBeEdited.containsKey(openOffer.getId())) { + if (openOffer.isPending()) { + resultHandler.handleResult(); // ignore if pending + } else if (offersToBeEdited.containsKey(openOffer.getId())) { + errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited."); + } else { Offer offer = openOffer.getOffer(); offerBookService.activateOffer(offer, () -> { @@ -595,8 +596,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe resultHandler.handleResult(); }, errorMessageHandler); - } else { - errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited."); } } @@ -858,26 +857,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // Place offer helpers /////////////////////////////////////////////////////////////////////////////////////////// - private void processScheduledOffers(TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler + private void processPendingOffers(boolean skipOffersWithTooManyAttempts) { + processPendingOffers(skipOffersWithTooManyAttempts, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing pending offers: " + errorMessage); + }); + } + + private void processPendingOffers(boolean skipOffersWithTooManyAttempts, + TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { synchronized (processOffersLock) { List errorMessages = new ArrayList(); List openOffers = getOpenOffers(); - for (OpenOffer scheduledOffer : openOffers) { - if (scheduledOffer.getState() != OpenOffer.State.SCHEDULED) continue; + for (OpenOffer pendingOffer : openOffers) { + if (pendingOffer.getState() != OpenOffer.State.PENDING) continue; + if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts CountDownLatch latch = new CountDownLatch(1); - processUnpostedOffer(openOffers, scheduledOffer, (transaction) -> { + processPendingOffer(openOffers, pendingOffer, (transaction) -> { latch.countDown(); }, errorMessage -> { - if (!scheduledOffer.isCanceled()) { - log.warn("Error processing unposted offer, offerId={}, attempt={}/{}, error={}", scheduledOffer.getId(), scheduledOffer.getNumProcessingAttempts(), MAX_PROCESS_ATTEMPTS, errorMessage); - if (scheduledOffer.getNumProcessingAttempts() >= MAX_PROCESS_ATTEMPTS) { - log.warn("Offer canceled after {} attempts, offerId={}, error={}", scheduledOffer.getNumProcessingAttempts(), scheduledOffer.getId(), errorMessage); - HavenoUtils.havenoSetup.getTopErrorMsg().set("Offer canceled after " + scheduledOffer.getNumProcessingAttempts() + " attempts. Please switch to a better Monero connection and try again.\n\nOffer ID: " + scheduledOffer.getId() + "\nError: " + errorMessage); - doCancelOffer(scheduledOffer); - } + if (!pendingOffer.isCanceled()) { + log.warn("Error processing pending offer, offerId={}, attempt={}, error={}", pendingOffer.getId(), pendingOffer.getNumProcessingAttempts(), errorMessage); errorMessages.add(errorMessage); + + // cancel offer if invalid + if (pendingOffer.getOffer().getState() == Offer.State.INVALID) { + log.warn("Canceling offer because it's invalid: {}", pendingOffer.getId()); + doCancelOffer(pendingOffer); + } } latch.countDown(); }); @@ -890,7 +898,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, THREAD_ID); } - private void processUnpostedOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void processPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing if (openOffer.isProcessing()) { @@ -900,17 +908,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // process offer openOffer.setProcessing(true); - doProcessUnpostedOffer(openOffers, openOffer, (transaction) -> { + doProcessPendingOffer(openOffers, openOffer, (transaction) -> { openOffer.setProcessing(false); resultHandler.handleResult(transaction); }, (errorMsg) -> { openOffer.setProcessing(false); openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1); + openOffer.getOffer().setErrorMessage(errorMsg); errorMessageHandler.handleErrorMessage(errorMsg); }); } - private void doProcessUnpostedOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void doProcessPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { new Thread(() -> { try { @@ -1075,7 +1084,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact()); openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash())); openOffer.setScheduledAmount(openOffer.getOffer().getAmountNeeded().toString()); - openOffer.setState(OpenOffer.State.SCHEDULED); + openOffer.setState(OpenOffer.State.PENDING); } private void scheduleWithEarliestTxs(List openOffers, OpenOffer openOffer) { @@ -1106,13 +1115,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // schedule txs openOffer.setScheduledTxHashes(scheduledTxHashes); openOffer.setScheduledAmount(scheduledAmount.toString()); - openOffer.setState(OpenOffer.State.SCHEDULED); + openOffer.setState(OpenOffer.State.PENDING); } private BigInteger getScheduledAmount(List openOffers) { BigInteger scheduledAmount = BigInteger.ZERO; for (OpenOffer openOffer : openOffers) { - if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue; + if (openOffer.getState() != OpenOffer.State.PENDING) continue; if (openOffer.getScheduledTxHashes() == null) continue; List fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes()); for (MoneroTxWallet fundingTx : fundingTxs) { @@ -1129,7 +1138,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private boolean isTxScheduledByOtherOffer(List openOffers, OpenOffer openOffer, String txHash) { for (OpenOffer otherOffer : openOffers) { if (otherOffer == openOffer) continue; - if (otherOffer.getState() != OpenOffer.State.SCHEDULED) continue; + if (otherOffer.getState() != OpenOffer.State.PENDING) continue; if (otherOffer.getScheduledTxHashes() == null) continue; for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) { if (txHash.equals(scheduledTxHash)) return true; @@ -1732,25 +1741,29 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }); } else { - // cancel and recreate offer - doCancelOffer(openOffer); - Offer updatedOffer = new Offer(openOffer.getOffer().getOfferPayload()); - updatedOffer.setPriceFeedService(priceFeedService); - OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, openOffer.getTriggerPrice()); + // reset offer state to pending + openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + openOffer.getOffer().setState(Offer.State.UNKNOWN); + openOffer.setState(OpenOffer.State.PENDING); - // repost offer + // republish offer synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); - addOpenOffer(updatedOpenOffer); - processUnpostedOffer(getOpenOffers(), updatedOpenOffer, (transaction) -> { + processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); if (completeHandler != null) completeHandler.run(); }, (errorMessage) -> { - if (!updatedOpenOffer.isCanceled()) { - log.warn("Error reposting offer {}: {}", updatedOpenOffer.getId(), errorMessage); - doCancelOffer(updatedOpenOffer); - updatedOffer.setErrorMessage(errorMessage); + if (!openOffer.isCanceled()) { + log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); + openOffer.getOffer().setErrorMessage(errorMessage); + + // cancel offer if invalid + if (openOffer.getOffer().getState() == Offer.State.INVALID) { + log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); + doCancelOffer(openOffer); + } } latch.countDown(); if (completeHandler != null) completeHandler.run(); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 36759cc1..27103cb6 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -66,7 +66,7 @@ public class MakerReserveOfferFunds extends Task { synchronized (XmrWalletService.WALLET_LOCK) { // reset protocol timeout - verifyScheduled(); + verifyPending(); model.getProtocol().startTimeoutTimer(); // collect relevant info @@ -94,7 +94,7 @@ public class MakerReserveOfferFunds extends Task { } // verify still open - verifyScheduled(); + verifyPending(); if (reserveTx != null) break; } } @@ -104,6 +104,7 @@ public class MakerReserveOfferFunds extends Task { model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId()); if (reserveTx != null) { model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + offer.getOfferPayload().setReserveTxKeyImages(null); } throw e; @@ -131,7 +132,7 @@ public class MakerReserveOfferFunds extends Task { } } - public void verifyScheduled() { - if (!model.getOpenOffer().isScheduled()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); + public void verifyPending() { + if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); } } diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java index e252e51d..2037b51d 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java @@ -116,9 +116,10 @@ public class MakerSendSignOfferRequest extends Task { model.getOpenOffer().getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress); model.getOpenOffer().getOffer().setState(Offer.State.OFFER_FEE_RESERVED); resultHandler.handleResult(); - } else { - errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage()); - } + } else { + model.getOpenOffer().getOffer().setState(Offer.State.INVALID); + errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage()); + } } }; model.getP2PService().addDecryptedDirectMessageListener(ackListener); @@ -137,9 +138,9 @@ public class MakerSendSignOfferRequest extends Task { log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage); excludedArbitrators.add(arbitratorNodeAddress); - // check if offer still scheduled - if (!model.getOpenOffer().isScheduled()) { - errorMessageHandler.handleErrorMessage("Offer is no longer scheduled, offerId=" + model.getOpenOffer().getId()); + // check if offer still pending + if (!model.getOpenOffer().isPending()) { + errorMessageHandler.handleErrorMessage("Offer is no longer pending, offerId=" + model.getOpenOffer().getId()); return; } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index d3f2d787..c1f55663 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -296,7 +296,7 @@ public class OpenOffersView extends ActivatableViewAndModel availableItems = sortedList.stream() - .filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isScheduled()) + .filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isPending()) .collect(Collectors.toList()); if (availableItems.size() == 0) { selectToggleButton.setDisable(true); @@ -710,7 +710,7 @@ public class OpenOffersView extends ActivatableViewAndModel Date: Wed, 17 Jul 2024 12:26:14 -0400 Subject: [PATCH 38/58] reduce logging on failure to sign offer --- .../haveno/core/offer/OpenOfferManager.java | 24 +++++++++---------- .../offer/placeoffer/PlaceOfferProtocol.java | 1 - 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 58581118..b100de41 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -473,14 +473,16 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); // process pending offers - processPendingOffers(false); + processPendingOffers(false, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing pending offers on bootstrap: " + errorMessage); + }); // register to process pending offers on new block xmrWalletService.addWalletListener(new MoneroWalletListener() { @Override public void onNewBlock(long height) { - // process pending offers on new block a few times + // process each pending offer on new block a few times, then rely on period republish processPendingOffers(true, (transaction) -> {}, (errorMessage) -> { log.warn("Error processing pending offers on new block {}: {}", height, errorMessage); }); @@ -856,13 +858,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe /////////////////////////////////////////////////////////////////////////////////////////// // Place offer helpers /////////////////////////////////////////////////////////////////////////////////////////// - - private void processPendingOffers(boolean skipOffersWithTooManyAttempts) { - processPendingOffers(skipOffersWithTooManyAttempts, (transaction) -> {}, (errorMessage) -> { - log.warn("Error processing pending offers: " + errorMessage); - }); - } - private void processPendingOffers(boolean skipOffersWithTooManyAttempts, TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler ErrorMessageHandler errorMessageHandler) { @@ -878,8 +873,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe latch.countDown(); }, errorMessage -> { if (!pendingOffer.isCanceled()) { - log.warn("Error processing pending offer, offerId={}, attempt={}, error={}", pendingOffer.getId(), pendingOffer.getNumProcessingAttempts(), errorMessage); - errorMessages.add(errorMessage); + String warnMessage = "Error processing pending offer, offerId=" + pendingOffer.getId() + ", attempt=" + pendingOffer.getNumProcessingAttempts() + ": " + errorMessage; + errorMessages.add(warnMessage); // cancel offer if invalid if (pendingOffer.getOffer().getState() == Offer.State.INVALID) { @@ -892,8 +887,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe HavenoUtils.awaitLatch(latch); } requestPersistence(); - if (errorMessages.size() > 0) errorMessageHandler.handleErrorMessage(errorMessages.toString()); - else resultHandler.handleResult(null); + if (errorMessages.isEmpty()) { + if (resultHandler != null) resultHandler.handleResult(null); + } else { + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString()); + } } }, THREAD_ID); } diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java index e6b62fdc..0b22d9e4 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java @@ -159,7 +159,6 @@ public class PlaceOfferProtocol { if (timeoutTimer != null) { taskRunner.cancel(); if (!model.getOpenOffer().isCanceled()) { - log.error(errorMessage); model.getOpenOffer().getOffer().setErrorMessage(errorMessage); } stopTimeoutTimer(); From caaf9f7b5bc8a33b43f720a1516c8a0ac10de63e Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 17 Jul 2024 17:48:25 -0400 Subject: [PATCH 39/58] limit logging of poll errors to once every 4 minutes --- .../haveno/core/api/XmrConnectionService.java | 22 +++++++++---------- .../java/haveno/core/trade/HavenoUtils.java | 5 ++++- .../core/xmr/wallet/XmrKeyImagePoller.java | 10 ++++++++- .../core/xmr/wallet/XmrWalletService.java | 8 +++---- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index f4122e4a..276a8517 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -70,8 +70,6 @@ public final class XmrConnectionService { private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor - private static final long LOG_POLL_ERROR_AFTER_MS = 300000; // minimum period between logging errors fetching daemon info - private static Long lastErrorTimestamp; private final Object lock = new Object(); private final Object pollLock = new Object(); @@ -100,6 +98,7 @@ public final class XmrConnectionService { private Boolean isConnected = false; @Getter private MoneroDaemonInfo lastInfo; + private Long lastLogPollErrorTimestamp; private Long syncStartHeight = null; private TaskLooper daemonPollLooper; @Getter @@ -680,8 +679,14 @@ public final class XmrConnectionService { return; } + // log error message periodically + if ((lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS)) { + log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage()); + if (DevEnv.isDevMode()) e.printStackTrace(); + lastLogPollErrorTimestamp = System.currentTimeMillis(); + } + // switch to best connection - log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage()); switchToBestConnection(); lastInfo = daemon.getInfo(); // caught internally if still fails } @@ -722,9 +727,9 @@ public final class XmrConnectionService { }); // handle error recovery - if (lastErrorTimestamp != null) { + if (lastLogPollErrorTimestamp != null) { log.info("Successfully fetched daemon info after previous error"); - lastErrorTimestamp = null; + lastLogPollErrorTimestamp = null; } // clear error message @@ -737,13 +742,6 @@ public final class XmrConnectionService { // skip if shut down if (isShutDownStarted) return; - // log error message periodically - if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > LOG_POLL_ERROR_AFTER_MS)) { - lastErrorTimestamp = System.currentTimeMillis(); - log.warn("Could not update daemon info: " + e.getMessage()); - if (DevEnv.isDevMode()) e.printStackTrace(); - } - // set error message getConnectionServiceErrorMsg().set(e.getMessage()); } finally { diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 351d6bff..ff8de230 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -78,6 +78,9 @@ public class HavenoUtils { public static final double TAKER_FEE_PCT = 0.0075; // 0.75% public static final double PENALTY_FEE_PCT = 0.02; // 2% + // other configuration + public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes + // synchronize requests to the daemon private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool) private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create txs) @@ -99,7 +102,7 @@ public class HavenoUtils { public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("##############0.000000000000", DECIMAL_FORMAT_SYMBOLS); public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); - // TODO: better way to share references? + // shared references TODO: better way to share references? public static HavenoSetup havenoSetup; public static ArbitrationManager arbitrationManager; public static XmrWalletService xmrWalletService; diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java index 8c411158..731c1311 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -32,6 +32,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import haveno.core.trade.HavenoUtils; + /** * Poll for changes to the spent status of key images. * @@ -47,6 +49,7 @@ public class XmrKeyImagePoller { private TaskLooper looper; private Map lastStatuses = new HashMap(); private boolean isPolling = false; + private Long lastLogPollErrorTimestamp; /** * Construct the listener. @@ -265,7 +268,12 @@ public class XmrKeyImagePoller { spentStatuses = daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } } catch (Exception e) { - log.warn("Error polling spent status of key images: " + e.getMessage()); + + // limit error logging + if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { + log.warn("Error polling spent status of key images: " + e.getMessage()); + lastLogPollErrorTimestamp = System.currentTimeMillis(); + } return; } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index d135526a..96ce12e5 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -134,8 +134,6 @@ public class XmrWalletService { private static final String THREAD_ID = XmrWalletService.class.getSimpleName(); private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final long NUM_BLOCKS_BEHIND_TOLERANCE = 5; - private static final long LOG_POLL_ERROR_AFTER_MS = 180000; // log poll error if unsuccessful after this time - private static Long lastPollSuccessTimestamp; private final User user; private final Preferences preferences; @@ -172,6 +170,7 @@ public class XmrWalletService { private TaskLooper pollLooper; private boolean pollInProgress; private Long pollPeriodMs; + private Long lastLogPollErrorTimestamp; private final Object pollLock = new Object(); private Long cachedHeight; private BigInteger cachedBalance; @@ -1846,11 +1845,12 @@ public class XmrWalletService { synchronized (HavenoUtils.getDaemonLock()) { try { cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); - lastPollSuccessTimestamp = System.currentTimeMillis(); + lastLogPollErrorTimestamp = null; } catch (Exception e) { // fetch from pool can fail if (!isShutDownStarted) { - if (lastPollSuccessTimestamp == null || System.currentTimeMillis() - lastPollSuccessTimestamp > LOG_POLL_ERROR_AFTER_MS) { // only log if not recently successful + if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { // limit error logging log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); + lastLogPollErrorTimestamp = System.currentTimeMillis(); } } } From 7308206a10c574f8aa76fb031ca362f18708a028 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 17 Jul 2024 17:27:07 -0400 Subject: [PATCH 40/58] set trade role from utility class in api #1146 --- core/src/main/java/haveno/core/api/CoreApi.java | 4 ---- core/src/main/java/haveno/core/api/model/TradeInfo.java | 6 ++---- core/src/main/java/haveno/core/trade/TradeUtil.java | 4 ++-- .../src/main/java/haveno/daemon/grpc/GrpcTradesService.java | 3 +-- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index fac92b31..25f98c34 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -561,10 +561,6 @@ public class CoreApi { return coreTradesService.getTrades(); } - public String getTradeRole(String tradeId) { - return coreTradesService.getTradeRole(tradeId); - } - public List getChatMessages(String tradeId) { return coreTradesService.getChatMessages(tradeId); } diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java index f53d3ee9..fa94fd27 100644 --- a/core/src/main/java/haveno/core/api/model/TradeInfo.java +++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java @@ -21,6 +21,7 @@ import haveno.common.Payload; import haveno.core.api.model.builder.TradeInfoV1Builder; import haveno.core.trade.Contract; import haveno.core.trade.Trade; +import haveno.core.trade.TradeUtil; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -142,10 +143,7 @@ public class TradeInfo implements Payload { } public static TradeInfo toTradeInfo(Trade trade) { - return toTradeInfo(trade, null); - } - - public static TradeInfo toTradeInfo(Trade trade, String role) { + String role = TradeUtil.getRole(trade); ContractInfo contractInfo; if (trade.getContract() != null) { Contract contract = trade.getContract(); diff --git a/core/src/main/java/haveno/core/trade/TradeUtil.java b/core/src/main/java/haveno/core/trade/TradeUtil.java index 1b55a6b2..bfebca48 100644 --- a/core/src/main/java/haveno/core/trade/TradeUtil.java +++ b/core/src/main/java/haveno/core/trade/TradeUtil.java @@ -172,7 +172,7 @@ public class TradeUtil { * @param trade Trade * @return String describing a trader's role for a given trade */ - public String getRole(Trade trade) { + public static String getRole(Trade trade) { Offer offer = trade.getOffer(); if (offer == null) throw new IllegalStateException(format("could not get role because no offer was found for trade '%s'", @@ -191,7 +191,7 @@ public class TradeUtil { * @param currencyCode String * @return String describing a trader's role */ - public String getRole(boolean isBuyerMakerAndSellerTaker, boolean isMaker, String currencyCode) { + private static String getRole(boolean isBuyerMakerAndSellerTaker, boolean isMaker, String currencyCode) { if (isTraditionalCurrency(currencyCode)) { String baseCurrencyCode = Res.getBaseCurrencyCode(); if (isBuyerMakerAndSellerTaker) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index e521a7ee..123078b2 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -96,9 +96,8 @@ class GrpcTradesService extends TradesImplBase { StreamObserver responseObserver) { try { Trade trade = coreApi.getTrade(req.getTradeId()); - String role = coreApi.getTradeRole(req.getTradeId()); var reply = GetTradeReply.newBuilder() - .setTrade(toTradeInfo(trade, role).toProtoMessage()) + .setTrade(toTradeInfo(trade).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); From 0b3763f900746c5fa9b9e0f468b52c3e15822c5f Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 18 Jul 2024 12:57:18 -0400 Subject: [PATCH 41/58] fix 'not enough money' bug by trying any subaddress --- .../src/main/java/haveno/core/xmr/wallet/XmrWalletService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 96ce12e5..5c53e37d 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -692,11 +692,12 @@ public class XmrWalletService { try { return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, subaddressIndices.get(i)); } catch (Exception e) { - if (i == subaddressIndices.size() - 1 && reserveExactAmount) throw e; // throw if no subaddress with exact output + log.info("Cannot create trade tx from preferred subaddress index " + subaddressIndices.get(i) + ": " + e.getMessage()); } } // try any subaddress + if (!subaddressIndices.isEmpty()) log.info("Could not create trade tx from preferred subaddresses, trying any subaddress"); return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, null); } } From 326cfdfb80ae41d4d9ea851716d981d38211aa32 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 19 Jul 2024 09:06:47 -0400 Subject: [PATCH 42/58] Revert "enable proof of work dos protection in torrc" This reverts commit c22c3b82dd43b66b30e67ac25b2488b704fb83b3. --- relay/torrc | 1 - 1 file changed, 1 deletion(-) diff --git a/relay/torrc b/relay/torrc index 29f757a7..3bfc6285 100644 --- a/relay/torrc +++ b/relay/torrc @@ -1,3 +1,2 @@ HiddenServiceDir build/tor-hidden-service HiddenServicePort 80 127.0.0.1:8080 -HiddenServicePoWDefensesEnabled 1 From 16263bb7b36eb79c7f18eaa51e24ef4fb63db37d Mon Sep 17 00:00:00 2001 From: shortwavesurfer2009 <116814522+shortwavesurfer2009@users.noreply.github.com> Date: Fri, 19 Jul 2024 19:07:31 -0400 Subject: [PATCH 43/58] Update deployment documentation (#1152) --- docs/deployment-guide.md | 44 +++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 3a814dd8..92b28944 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -24,7 +24,38 @@ Some good hints about how to secure a VPS are in [Monero's meta repository](http ## Fork and build Haveno -First fork Haveno to a public repository. Then build Haveno: +### Install dependencies + +On Linux and macOS, install Java JDK 21: + +`curl -s "https://get.sdkman.io" | bash` +`sdk install java 21.0.2.fx-librca` + +On Windows, install MSYS2 and Java JDK 21: + +Install MSYS2. + +Start MSYS2 MINGW64 or MSYS MINGW32 depending on your system. Use MSYS2 for all commands throughout this document. + +Update pacman: `pacman -Syy` + +Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. + +64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git` + +32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git` + +`curl -s "https://get.sdkman.io" | bash` + +`sdk install java 21.0.2.fx-librca` + +### Alternative Instructions + +#### Ubuntu 22.04 + +`sudo apt-get install openjdk-21-jdk` + +Fork Haveno to a public repository. Then build Haveno: ``` git clone @@ -57,9 +88,12 @@ For each seed node: 1. [Build the Haveno repository](#fork-and-build-haveno). 2. [Start a local Monero node](#start-a-local-monero-node). -3. Run `make seednode` to run a seednode on Monero's mainnet or `make seednode-stagenet` to run a seednode on Monero's stagenet. -4. The node will print its onion address to the console. Record the onion address in `core/src/main/resources/xmr_.seednodes`. Be careful to record full addresses correctly. -5. Update all seed nodes, arbitrators, and user applications for the change to take effect. +3. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed. +4. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`). +5. Run `sudo systemctl start haveno-seednode.service` to start the seednode and also run `sudo systemctl start haveno-seednode2.service` if you are the very first seed in a new network and coppied haveno-seednode2.service to your systemd folder. +6. Run `journalctl -u haveno-seednode.service -b -f` which will print the log and show the `.onion` address of the seed node. Press `Ctrl+C` to stop printing the log and record the `.onion` address given. +7. Add the `.onion` address to `core/src/main/resources/xmr_.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:1002)`. Be careful to record full addresses correctly. +8. Update all seed nodes, arbitrators, and user applications for the change to take effect. Customize and deploy haveno-seednode.service to run a seed node as a system service. @@ -222,4 +256,4 @@ Arbitrators can manually sign payment accounts. First open the legacy UI. ## Other tips * If a dispute does not open properly, try manually reopening the dispute with a keyboard shortcut: `ctrl + o`. -* To send a private notification to a peer: click the user icon and enter `alt + r`. Enter a private key which is registered to send private notifications. \ No newline at end of file +* To send a private notification to a peer: click the user icon and enter `alt + r`. Enter a private key which is registered to send private notifications. From 41e63805c1c3aa9da5e847bfc69f492e0a9a5647 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 19 Jul 2024 19:15:35 -0400 Subject: [PATCH 44/58] cleanup instructions to install dependencies in deployment guide --- docs/deployment-guide.md | 41 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 92b28944..97aaf046 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -22,38 +22,33 @@ Arbitrators can be started in a Screen session and then detached to run in the b Some good hints about how to secure a VPS are in [Monero's meta repository](https://github.com/monero-project/meta/blob/master/SERVER_SETUP_HARDENING.md). -## Fork and build Haveno - -### Install dependencies +## Install dependencies On Linux and macOS, install Java JDK 21: -`curl -s "https://get.sdkman.io" | bash` -`sdk install java 21.0.2.fx-librca` +``` +curl -s "https://get.sdkman.io" | bash +sdk install java 21.0.2.fx-librca +``` + +Alternatively, on Ubuntu 22.04: + +`sudo apt-get install openjdk-21-jdk` On Windows, install MSYS2 and Java JDK 21: -Install MSYS2. +1. Install [MSYS2](https://www.msys2.org/). +2. Start MSYS2 MINGW64 or MSYS MINGW32 depending on your system. Use MSYS2 for all commands throughout this document. +4. Update pacman: `pacman -Syy` +5. Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. -Start MSYS2 MINGW64 or MSYS MINGW32 depending on your system. Use MSYS2 for all commands throughout this document. + 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git` -Update pacman: `pacman -Syy` + 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git` +6. `curl -s "https://get.sdkman.io" | bash` +7. `sdk install java 21.0.2.fx-librca` -Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. - -64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git` - -32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git` - -`curl -s "https://get.sdkman.io" | bash` - -`sdk install java 21.0.2.fx-librca` - -### Alternative Instructions - -#### Ubuntu 22.04 - -`sudo apt-get install openjdk-21-jdk` +## Fork and build Haveno Fork Haveno to a public repository. Then build Haveno: From cb132e727a0e5edb28dc6e0ca6d4d76cee865944 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 18 Jul 2024 07:30:27 -0400 Subject: [PATCH 45/58] limit switch connection requests to 3 per minute --- .../haveno/core/api/XmrConnectionService.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 276a8517..8e6a22c9 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -107,9 +107,9 @@ public final class XmrConnectionService { // connection switching private static final int EXCLUDE_CONNECTION_SECONDS = 300; - private static final int SKIP_SWITCH_WITHIN_MS = 60000; + private static final int MAX_SWITCH_REQUESTS_PER_MINUTE = 3; private Set excludedConnections = new HashSet<>(); - private long lastSwitchRequestTimestamp; + private int numRequestsLastMinute; @Inject public XmrConnectionService(P2PService p2PService, @@ -279,24 +279,27 @@ public final class XmrConnectionService { return false; } - // skip if last switch was too recent - boolean skipSwitch = System.currentTimeMillis() - lastSwitchRequestTimestamp < SKIP_SWITCH_WITHIN_MS; - lastSwitchRequestTimestamp = System.currentTimeMillis(); - if (skipSwitch) { - log.warn("Skipping switch to next best Monero connection because last switch was less than {} seconds ago", SKIP_SWITCH_WITHIN_MS / 1000); - lastSwitchRequestTimestamp = System.currentTimeMillis(); + // skip if too many requests in the last minute + if (numRequestsLastMinute > MAX_SWITCH_REQUESTS_PER_MINUTE) { + log.warn("Skipping switch to next best Monero connection because more than {} requests were made in the last minute", MAX_SWITCH_REQUESTS_PER_MINUTE); return false; } - // try to get connection to switch to + // increment request count + numRequestsLastMinute++; + UserThread.runAfter(() -> numRequestsLastMinute--, 60); // decrement after one minute + + // exclude current connection MoneroRpcConnection currentConnection = getConnection(); if (currentConnection != null) excludedConnections.add(currentConnection); + + // get connection to switch to MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections); // remove from excluded connections after period UserThread.runAfter(() -> { if (currentConnection != null) excludedConnections.remove(currentConnection); - }, EXCLUDE_CONNECTION_SECONDS); + }, EXCLUDE_CONNECTION_SECONDS); // switch to best connection if (bestConnection == null) { From ebcadb7bedf460ca20058cfb42fcc8f7855d764d Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 18 Jul 2024 09:14:16 -0400 Subject: [PATCH 46/58] reinitialize main wallet on same thread as connection change --- .../core/xmr/wallet/XmrWalletService.java | 175 +++++++++--------- 1 file changed, 88 insertions(+), 87 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 5c53e37d..aa442265 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1304,100 +1304,102 @@ public class XmrWalletService { } private void maybeInitMainWallet(boolean sync, int numAttempts) { - ThreadUtils.execute(() -> { - synchronized (WALLET_LOCK) { - if (isShutDownStarted) return; - - // open or create wallet main wallet - if (wallet == null) { - MoneroDaemonRpc daemon = xmrConnectionService.getDaemon(); - log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri())); - if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { - wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced)); - } else if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { - wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort); - - // set wallet creation date to yesterday to guarantee complete restore - LocalDateTime localDateTime = LocalDate.now().atStartOfDay().minusDays(1); - long date = localDateTime.toEpochSecond(ZoneOffset.UTC); - user.setWalletCreationDate(date); - } - isClosingWallet = false; + ThreadUtils.execute(() -> doMaybeInitMainWallet(sync, numAttempts), THREAD_ID); + } + + private void doMaybeInitMainWallet(boolean sync, int numAttempts) { + synchronized (WALLET_LOCK) { + if (isShutDownStarted) return; + + // open or create wallet main wallet + if (wallet == null) { + MoneroDaemonRpc daemon = xmrConnectionService.getDaemon(); + log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri())); + if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { + wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced)); + } else if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { + wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort); + + // set wallet creation date to yesterday to guarantee complete restore + LocalDateTime localDateTime = LocalDate.now().atStartOfDay().minusDays(1); + long date = localDateTime.toEpochSecond(ZoneOffset.UTC); + user.setWalletCreationDate(date); } - - // sync wallet and register listener - if (wallet != null && !isShutDownStarted) { - log.info("Monero wallet path={}", wallet.getPath()); - - // sync main wallet if applicable - if (sync && numAttempts > 0) { - try { + isClosingWallet = false; + } - // switch connection if disconnected - if (!wallet.isConnectedToDaemon()) { - log.warn("Switching connection before syncing with progress because disconnected"); - if (requestSwitchToNextBestConnection()) return; // calls back to this method - } - - // sync main wallet - log.info("Syncing main wallet"); - long time = System.currentTimeMillis(); - syncWithProgress(); // blocking - log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); + // sync wallet and register listener + if (wallet != null && !isShutDownStarted) { + log.info("Monero wallet path={}", wallet.getPath()); - // poll wallet - doPollWallet(true); - if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); - - // log wallet balances - if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { - BigInteger balance = getBalance(); - BigInteger unlockedBalance = getAvailableBalance(); - log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); - } - - // reapply connection after wallet synced (might reinitialize wallet on new thread) - ThreadUtils.execute(() -> onConnectionChanged(xmrConnectionService.getConnection()), THREAD_ID); - - // reset internal state if main wallet was swapped - resetIfWalletChanged(); - - // signal that main wallet is synced - doneDownload(); - - // notify setup that main wallet is initialized - // TODO: app fully initializes after this is set to true, even though wallet might not be initialized if unconnected. wallet will be created when connection detected - // refactor startup to call this and sync off main thread? but the calls to e.g. getBalance() fail with 'wallet and network is not yet initialized' + // sync main wallet if applicable + if (sync && numAttempts > 0) { + try { + + // switch connection if disconnected + if (!wallet.isConnectedToDaemon()) { + log.warn("Switching connection before syncing with progress because disconnected"); + if (requestSwitchToNextBestConnection()) return; // calls back to this method + } + + // sync main wallet + log.info("Syncing main wallet"); + long time = System.currentTimeMillis(); + syncWithProgress(); // blocking + log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); + + // poll wallet + doPollWallet(true); + if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); + + // log wallet balances + if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { + BigInteger balance = getBalance(); + BigInteger unlockedBalance = getAvailableBalance(); + log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); + } + + // reapply connection after wallet synced (might reinitialize wallet on new thread) + ThreadUtils.execute(() -> onConnectionChanged(xmrConnectionService.getConnection()), THREAD_ID); + + // reset internal state if main wallet was swapped + resetIfWalletChanged(); + + // signal that main wallet is synced + doneDownload(); + + // notify setup that main wallet is initialized + // TODO: app fully initializes after this is set to true, even though wallet might not be initialized if unconnected. wallet will be created when connection detected + // refactor startup to call this and sync off main thread? but the calls to e.g. getBalance() fail with 'wallet and network is not yet initialized' + HavenoUtils.havenoSetup.getWalletInitialized().set(true); + + // save but skip backup on initialization + saveMainWallet(false); + } catch (Exception e) { + if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized + log.warn("Error initially syncing main wallet: {}", e.getMessage()); + if (numAttempts <= 1) { + log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts); HavenoUtils.havenoSetup.getWalletInitialized().set(true); - - // save but skip backup on initialization saveMainWallet(false); - } catch (Exception e) { - if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized - log.warn("Error initially syncing main wallet: {}", e.getMessage()); - if (numAttempts <= 1) { - log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts); - HavenoUtils.havenoSetup.getWalletInitialized().set(true); - saveMainWallet(false); - - // reschedule to init main wallet - UserThread.runAfter(() -> { - maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); - }, xmrConnectionService.getRefreshPeriodMs() / 1000); - } else { - log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); - UserThread.runAfter(() -> { - maybeInitMainWallet(true, numAttempts - 1); - }, xmrConnectionService.getRefreshPeriodMs() / 1000); - } + + // reschedule to init main wallet + UserThread.runAfter(() -> { + maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); + }, xmrConnectionService.getRefreshPeriodMs() / 1000); + } else { + log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); + UserThread.runAfter(() -> { + maybeInitMainWallet(true, numAttempts - 1); + }, xmrConnectionService.getRefreshPeriodMs() / 1000); } } - - // start polling main wallet - startPolling(); } + + // start polling main wallet + startPolling(); } - }, THREAD_ID); + } } private void resetIfWalletChanged() { @@ -1679,7 +1681,7 @@ public class XmrWalletService { } else { log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet closeMainWallet(true); - maybeInitMainWallet(false); + doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS); return; // wallet is re-initialized } } else { @@ -1846,7 +1848,6 @@ public class XmrWalletService { synchronized (HavenoUtils.getDaemonLock()) { try { cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); - lastLogPollErrorTimestamp = null; } catch (Exception e) { // fetch from pool can fail if (!isShutDownStarted) { if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { // limit error logging From 09fd8710b17e35433bf43a9ab9cc24ef190d6897 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 18 Jul 2024 10:00:40 -0400 Subject: [PATCH 47/58] do not set offer state to pending after canceled --- core/src/main/java/haveno/core/offer/OpenOfferManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index b100de41..4a4610d8 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -625,6 +625,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.info("Canceling open offer: {}", openOffer.getId()); if (!offersToBeEdited.containsKey(openOffer.getId())) { if (openOffer.isAvailable()) { + openOffer.setState(OpenOffer.State.CANCELED); offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), () -> { ThreadUtils.submitToPool(() -> { // TODO: this runs off thread and then shows popup when done. should show overlay spinner until done @@ -634,6 +635,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, errorMessageHandler); } else { + openOffer.setState(OpenOffer.State.CANCELED); ThreadUtils.submitToPool(() -> { doCancelOffer(openOffer); if (resultHandler != null) resultHandler.handleResult(); @@ -1082,7 +1084,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact()); openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash())); openOffer.setScheduledAmount(openOffer.getOffer().getAmountNeeded().toString()); - openOffer.setState(OpenOffer.State.PENDING); + if (!openOffer.isCanceled()) openOffer.setState(OpenOffer.State.PENDING); } private void scheduleWithEarliestTxs(List openOffers, OpenOffer openOffer) { From 9b26682646ac8d528a71b073e22c2bdfdc5f4192 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 19 Jul 2024 08:47:04 -0400 Subject: [PATCH 48/58] fixes checking for missing wallet data --- core/src/main/java/haveno/core/trade/Trade.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 0d814b51..afa75792 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2578,7 +2578,7 @@ public abstract class Trade implements Tradable, Model { if (isConnectionRefused) forceRestartTradeWallet(); else { boolean isWalletConnected = isWalletConnectedToDaemon(); - if (!isShutDownStarted && wallet != null && isWalletConnected) { + if (wallet != null && !isShutDownStarted && isWalletConnected) { log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); requestSwitchToNextBestConnection(); //e.printStackTrace(); @@ -2622,6 +2622,9 @@ public abstract class Trade implements Tradable, Model { // force restart wallet forceRestartTradeWallet(); + // skip if payout published in the meantime + if (isPayoutPublished()) return; + // rescan blockchain with global daemon lock synchronized (HavenoUtils.getDaemonLock()) { Long timeout = null; @@ -2651,11 +2654,14 @@ public abstract class Trade implements Tradable, Model { // import multisig hex log.warn("Importing multisig hex to recover wallet data for {} {}", getClass().getSimpleName(), getShortId()); importMultisigHex(); + + // poll wallet + doPollWallet(); + + // check again if missing data + if (isWalletMissingData()) throw new IllegalStateException("Wallet is still missing data after attempting recovery for " + getClass().getSimpleName() + " " + getShortId()); } } - - // check again after releasing lock - if (isWalletMissingData()) throw new IllegalStateException("Wallet is still missing data after attempting recovery for " + getClass().getSimpleName() + " " + getShortId()); } private boolean isWalletMissingData() { @@ -2670,6 +2676,8 @@ public abstract class Trade implements Tradable, Model { return true; } if (wallet.getBalance().equals(BigInteger.ZERO)) { + doPollWallet(); // poll once more to be sure + if (isPayoutPublished()) return false; // payout can become published while checking balance log.warn("Wallet balance is zero for {} {}", getClass().getSimpleName(), getId()); return true; } From 291622e4529c6fec3198226b210a69eb15c0da6d Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 19 Jul 2024 08:47:33 -0400 Subject: [PATCH 49/58] switch to current connection on connection changed --- core/src/main/java/haveno/core/trade/Trade.java | 3 +++ .../main/java/haveno/core/xmr/wallet/XmrWalletService.java | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index afa75792..efb62c39 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2323,6 +2323,9 @@ public abstract class Trade implements Tradable, Model { private void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { + // use current connection + connection = xmrConnectionService.getConnection(); + // check if ignored if (isShutDownStarted) return; if (getWallet() == null) return; diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index aa442265..f838c92b 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1660,6 +1660,11 @@ public class XmrWalletService { private void onConnectionChanged(MoneroRpcConnection connection) { synchronized (WALLET_LOCK) { + + // use current connection + connection = xmrConnectionService.getConnection(); + + // check if ignored if (wallet == null || isShutDownStarted) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); From 82d586ab78659338a08c6d2f7a9f891177ed9c52 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 18 Jul 2024 10:15:59 -0400 Subject: [PATCH 50/58] remove trade on error force closes and shuts down thread --- .../main/java/haveno/core/trade/Trade.java | 10 ++++ .../java/haveno/core/trade/TradeManager.java | 50 ++++++++++--------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index efb62c39..81128164 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1657,6 +1657,16 @@ public abstract class Trade implements Tradable, Model { private void removeTradeOnError() { log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); + // force close wallet in case stuck + forceCloseWallet(); + + // shut down trade thread + try { + ThreadUtils.shutDown(getId(), 1000l); + } catch (Exception e) { + log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + } + // clear and shut down trade clearAndShutDown(); diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 54480b9d..b88330a3 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -284,23 +284,25 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void onDirectMessage(DecryptedMessageWithPubKey message, NodeAddress peer) { + public void onDirectMessage(DecryptedMessageWithPubKey message, NodeAddress sender) { NetworkEnvelope networkEnvelope = message.getNetworkEnvelope(); if (!(networkEnvelope instanceof TradeMessage)) return; - String tradeId = ((TradeMessage) networkEnvelope).getOfferId(); + TradeMessage tradeMessage = (TradeMessage) networkEnvelope; + String tradeId = tradeMessage.getOfferId(); + log.info("TradeManager received {} for tradeId={}, sender={}, uid={}", networkEnvelope.getClass().getSimpleName(), tradeId, sender, tradeMessage.getUid()); ThreadUtils.execute(() -> { if (networkEnvelope instanceof InitTradeRequest) { - handleInitTradeRequest((InitTradeRequest) networkEnvelope, peer); + handleInitTradeRequest((InitTradeRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof InitMultisigRequest) { - handleInitMultisigRequest((InitMultisigRequest) networkEnvelope, peer); + handleInitMultisigRequest((InitMultisigRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof SignContractRequest) { - handleSignContractRequest((SignContractRequest) networkEnvelope, peer); + handleSignContractRequest((SignContractRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof SignContractResponse) { - handleSignContractResponse((SignContractResponse) networkEnvelope, peer); + handleSignContractResponse((SignContractResponse) networkEnvelope, sender); } else if (networkEnvelope instanceof DepositRequest) { - handleDepositRequest((DepositRequest) networkEnvelope, peer); + handleDepositRequest((DepositRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof DepositResponse) { - handleDepositResponse((DepositResponse) networkEnvelope, peer); + handleDepositResponse((DepositResponse) networkEnvelope, sender); } }, tradeId); } @@ -538,7 +540,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { - log.info("Received InitTradeRequest from {} with tradeId {} and uid {}", sender, request.getOfferId(), request.getUid()); + log.info("TradeManager handling InitTradeRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -734,8 +736,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } - private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); + private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { + log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -750,11 +752,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } Trade trade = tradeOptional.get(); - getTradeProtocol(trade).handleInitMultisigRequest(request, peer); + getTradeProtocol(trade).handleInitMultisigRequest(request, sender); } - private void handleSignContractRequest(SignContractRequest request, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); + private void handleSignContractRequest(SignContractRequest request, NodeAddress sender) { + log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -769,11 +771,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } Trade trade = tradeOptional.get(); - getTradeProtocol(trade).handleSignContractRequest(request, peer); + getTradeProtocol(trade).handleSignContractRequest(request, sender); } - private void handleSignContractResponse(SignContractResponse request, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); + private void handleSignContractResponse(SignContractResponse request, NodeAddress sender) { + log.info("TradeManager handling SignContractResponse for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -788,11 +790,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } Trade trade = tradeOptional.get(); - ((TraderProtocol) getTradeProtocol(trade)).handleSignContractResponse(request, peer); + ((TraderProtocol) getTradeProtocol(trade)).handleSignContractResponse(request, sender); } - private void handleDepositRequest(DepositRequest request, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); + private void handleDepositRequest(DepositRequest request, NodeAddress sender) { + log.info("TradeManager handling DepositRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -807,11 +809,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } Trade trade = tradeOptional.get(); - ((ArbitratorProtocol) getTradeProtocol(trade)).handleDepositRequest(request, peer); + ((ArbitratorProtocol) getTradeProtocol(trade)).handleDepositRequest(request, sender); } - private void handleDepositResponse(DepositResponse response, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", response.getClass().getSimpleName(), response.getOfferId(), peer, response.getUid()); + private void handleDepositResponse(DepositResponse response, NodeAddress sender) { + log.info("TradeManager handling DepositResponse for tradeId={}, sender={}, uid={}", response.getOfferId(), sender, response.getUid()); try { Validator.nonEmptyStringOf(response.getOfferId()); @@ -829,7 +831,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } Trade trade = tradeOptional.get(); - ((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer); + ((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, sender); } /////////////////////////////////////////////////////////////////////////////////////////// From 5d739f912c02d708bb427c9382e33c9beb32917b Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 19 Jul 2024 08:49:10 -0400 Subject: [PATCH 51/58] remove dedicated connection change thread for trade --- .../haveno/core/api/XmrConnectionService.java | 2 +- .../main/java/haveno/core/trade/Trade.java | 46 +++++++++---------- .../core/xmr/wallet/XmrWalletService.java | 8 ++-- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 8e6a22c9..d44d3662 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -303,7 +303,7 @@ public final class XmrConnectionService { // switch to best connection if (bestConnection == null) { - log.warn("Could not get connection to switch to"); + log.warn("No connection to switch to"); return false; } setConnection(bestConnection); diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 81128164..cfd49309 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -627,7 +627,7 @@ public abstract class Trade implements Tradable, Model { // handle connection change on dedicated thread xmrConnectionService.addConnectionListener(connection -> { - ThreadUtils.execute(() -> onConnectionChanged(connection), getConnectionChangedThreadId()); + ThreadUtils.execute(() -> onConnectionChanged(connection), getId()); }); // reset buyer's payment sent state if no ack receive @@ -1501,7 +1501,6 @@ public abstract class Trade implements Tradable, Model { isShutDown = true; List shutDownThreads = new ArrayList<>(); shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); - shutDownThreads.add(() -> ThreadUtils.shutDown(getConnectionChangedThreadId())); ThreadUtils.awaitTasks(shutDownThreads); } @@ -2326,10 +2325,6 @@ public abstract class Trade implements Tradable, Model { return tradeVolumeProperty; } - private String getConnectionChangedThreadId() { - return getId() + ".onConnectionChanged"; - } - private void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { @@ -2399,24 +2394,29 @@ public abstract class Trade implements Tradable, Model { } private void syncWallet(boolean pollWallet) { - if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); - if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); - if (isWalletBehind()) { - log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId()); - long startTime = System.currentTimeMillis(); - syncWalletIfBehind(); - log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); - } - - // apply tor after wallet synced depending on configuration - if (!wasWalletSynced) { - wasWalletSynced = true; - if (xmrWalletService.isProxyApplied(wasWalletSynced)) { - onConnectionChanged(xmrConnectionService.getConnection()); + try { + if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); + if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); + if (isWalletBehind()) { + log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId()); + long startTime = System.currentTimeMillis(); + syncWalletIfBehind(); + log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); } + + // apply tor after wallet synced depending on configuration + if (!wasWalletSynced) { + wasWalletSynced = true; + if (xmrWalletService.isProxyApplied(wasWalletSynced)) { + onConnectionChanged(xmrConnectionService.getConnection()); + } + } + + if (pollWallet) pollWallet(); + } catch (Exception e) { + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); + throw e; } - - if (pollWallet) pollWallet(); } public void updatePollPeriod() { @@ -2593,7 +2593,7 @@ public abstract class Trade implements Tradable, Model { boolean isWalletConnected = isWalletConnectedToDaemon(); if (wallet != null && !isShutDownStarted && isWalletConnected) { log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); - requestSwitchToNextBestConnection(); + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); // do not block polling thread //e.printStackTrace(); } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index f838c92b..2e609d77 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1285,7 +1285,9 @@ public class XmrWalletService { else log.info(appliedMsg); // listen for connection changes - xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> onConnectionChanged(connection), THREAD_ID)); + xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> { + onConnectionChanged(connection); + }, THREAD_ID)); // initialize main wallet when daemon synced walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected(); @@ -1669,7 +1671,7 @@ public class XmrWalletService { if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String newProxyUri = connection == null ? null : connection.getProxyUri(); - log.info("Setting daemon connection for main wallet: uri={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri); + log.info("Setting daemon connection for main wallet, monerod={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri); // force restart main wallet if connection changed before synced if (!wasWalletSynced) { @@ -1706,7 +1708,7 @@ public class XmrWalletService { updatePollPeriod(); } - log.info("Done setting main wallet monerod=" + (wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getUri())); + log.info("Done setting daemon connection for main wallet, monerod=" + (wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getUri())); } } From eb8025f6e8afc41f3a3ec65682f2a066761b4676 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 19 Jul 2024 14:38:05 -0400 Subject: [PATCH 52/58] skip wallet checks on deletion if deposit not requested --- .../main/java/haveno/core/trade/Trade.java | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index cfd49309..bead9fea 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -967,29 +967,31 @@ public abstract class Trade implements Tradable, Model { if (walletExists()) { try { - // ensure wallet is initialized - boolean syncedWallet = false; - if (wallet == null) { - log.warn("Wallet is not initialized for {} {}, opening", getClass().getSimpleName(), getId()); - getWallet(); - syncWallet(true); - syncedWallet = true; - } + // check wallet state if deposit requested + if (isDepositRequested()) { - // sync wallet if deposit requested and payout not unlocked - if (isDepositRequested() && !isPayoutUnlocked() && !syncedWallet) { - log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId()); - syncWallet(true); - } - - // check if deposits published and payout not unlocked - if (isDepositsPublished() && !isPayoutUnlocked()) { - throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked"); - } - - // check for balance - if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { - synchronized (HavenoUtils.getDaemonLock()) { + // ensure wallet is initialized + boolean syncedWallet = false; + if (wallet == null) { + log.warn("Wallet is not initialized for {} {}, opening", getClass().getSimpleName(), getId()); + getWallet(); + syncWallet(true); + syncedWallet = true; + } + + // sync wallet if deposit requested and payout not unlocked + if (!isPayoutUnlocked() && !syncedWallet) { + log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId()); + syncWallet(true); + } + + // check if deposits published and payout not unlocked + if (isDepositsPublished() && !isPayoutUnlocked()) { + throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked"); + } + + // check for balance + if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { log.warn("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getId()); wallet.rescanSpent(); if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { @@ -1656,8 +1658,9 @@ public abstract class Trade implements Tradable, Model { private void removeTradeOnError() { log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); - // force close wallet in case stuck + // force close and re-open wallet in case stuck forceCloseWallet(); + if (isDepositRequested()) getWallet(); // shut down trade thread try { From fc3407cd50f4c90e3fbbce6ab8ac9f13d3da3972 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 20 Jul 2024 14:06:40 -0400 Subject: [PATCH 53/58] ignore switch connection request within 10s, 2 per min --- .../haveno/core/api/XmrConnectionService.java | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index d44d3662..caddf299 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -106,10 +106,12 @@ public final class XmrConnectionService { private List listeners = new ArrayList<>(); // connection switching - private static final int EXCLUDE_CONNECTION_SECONDS = 300; - private static final int MAX_SWITCH_REQUESTS_PER_MINUTE = 3; - private Set excludedConnections = new HashSet<>(); + private static final int EXCLUDE_CONNECTION_SECONDS = 180; + private static final int MAX_SWITCH_REQUESTS_PER_MINUTE = 2; + private static final int SKIP_SWITCH_WITHIN_MS = 10000; private int numRequestsLastMinute; + private long lastSwitchTimestamp; + private Set excludedConnections = new HashSet<>(); @Inject public XmrConnectionService(P2PService p2PService, @@ -270,15 +272,28 @@ public final class XmrConnectionService { if (bestConnection != null) setConnection(bestConnection); } - public boolean requestSwitchToNextBestConnection() { + public synchronized boolean requestSwitchToNextBestConnection() { log.warn("Request made to switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri()); + // skip if shut down started + if (isShutDownStarted) { + log.warn("Skipping switch to next best Monero connection because shut down has started"); + return false; + } + // skip if connection is fixed if (isFixedConnection() || !connectionManager.getAutoSwitch()) { log.info("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled"); return false; } + // skip if last switch was too recent + boolean skipSwitch = System.currentTimeMillis() - lastSwitchTimestamp < SKIP_SWITCH_WITHIN_MS; + if (skipSwitch) { + log.warn("Skipping switch to next best Monero connection because last switch was less than {} seconds ago", SKIP_SWITCH_WITHIN_MS / 1000); + return false; + } + // skip if too many requests in the last minute if (numRequestsLastMinute > MAX_SWITCH_REQUESTS_PER_MINUTE) { log.warn("Skipping switch to next best Monero connection because more than {} requests were made in the last minute", MAX_SWITCH_REQUESTS_PER_MINUTE); @@ -301,11 +316,14 @@ public final class XmrConnectionService { if (currentConnection != null) excludedConnections.remove(currentConnection); }, EXCLUDE_CONNECTION_SECONDS); - // switch to best connection + // return if no connection to switch to if (bestConnection == null) { log.warn("No connection to switch to"); return false; } + + // switch to best connection + lastSwitchTimestamp = System.currentTimeMillis(); setConnection(bestConnection); return true; } From 13d6eaee7dd04a0bac3ec0fba0ab7fcc74a033ff Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 20 Jul 2024 01:13:29 -0400 Subject: [PATCH 54/58] recover from offer funds being unexpectedly unavailable --- .../haveno/core/offer/OpenOfferManager.java | 67 ++++++++++++++----- .../tasks/MakerReserveOfferFunds.java | 2 +- .../arbitration/ArbitrationManager.java | 2 +- .../main/java/haveno/core/trade/Trade.java | 6 +- .../desktop/main/offer/MutableOfferView.java | 6 +- .../main/offer/MutableOfferViewModel.java | 3 + 6 files changed, 64 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 4a4610d8..ec8baae5 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -556,13 +556,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe latch.countDown(); resultHandler.handleResult(transaction); }, (errorMessage) -> { - if (openOffer.isCanceled()) latch.countDown(); - else { + if (!openOffer.isCanceled()) { log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage); doCancelOffer(openOffer); - latch.countDown(); - errorMessageHandler.handleErrorMessage(errorMessage); } + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); }); HavenoUtils.awaitLatch(latch); } @@ -943,8 +942,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // if not found, create tx to split exact output if (splitOutputTx == null) { - if (openOffer.getSplitOutputTxHash() != null) log.warn("Split output tx not found for offer {}", openOffer.getId()); - splitOrSchedule(openOffers, openOffer, amountNeeded); + if (openOffer.getSplitOutputTxHash() != null) { + log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); + setSplitOutputTx(openOffer, null); + } + try { + splitOrSchedule(openOffers, openOffer, amountNeeded); + } catch (Exception e) { + log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); + openOffer.getOffer().setState(Offer.State.INVALID); + errorMessageHandler.handleErrorMessage(e.getMessage()); + return; + } } else if (!splitOutputTx.isLocked()) { // otherwise sign and post offer if split output available @@ -981,7 +990,23 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // return split output tx if already assigned if (openOffer != null && openOffer.getSplitOutputTxHash() != null) { - return xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); + + // get recorded split output tx + MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); + + // check if split output tx is available for offer + if (splitOutputTx.isLocked()) return splitOutputTx; + else { + boolean isAvailable = true; + for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) { + if (output.isSpent() || output.isFrozen()) { + isAvailable = false; + break; + } + } + if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx; + else log.warn("Split output tx is no longer available for offer {}", openOffer.getId()); + } } // get split output tx to offer's preferred subaddress @@ -996,6 +1021,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs); } + private boolean isReservedByOffer(OpenOffer openOffer, MoneroTxWallet tx) { + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) return false; + Set offerKeyImages = new HashSet(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (offerKeyImages.contains(output.getKeyImage().getHex())) return true; + } + return false; + } + private List getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) { List splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false)); Set removeTxs = new HashSet(); @@ -1064,7 +1098,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); break; } catch (Exception e) { - log.warn("Error creating split output tx to fund offer {} at subaddress {}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds + log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying @@ -1080,10 +1115,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private void setSplitOutputTx(OpenOffer openOffer, MoneroTxWallet splitOutputTx) { - openOffer.setSplitOutputTxHash(splitOutputTx.getHash()); - openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact()); - openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash())); - openOffer.setScheduledAmount(openOffer.getOffer().getAmountNeeded().toString()); + openOffer.setSplitOutputTxHash(splitOutputTx == null ? null : splitOutputTx.getHash()); + openOffer.setSplitOutputTxFee(splitOutputTx == null ? 0l : splitOutputTx.getFee().longValueExact()); + openOffer.setScheduledTxHashes(splitOutputTx == null ? null : Arrays.asList(splitOutputTx.getHash())); + openOffer.setScheduledAmount(splitOutputTx == null ? null : openOffer.getOffer().getAmountNeeded().toString()); if (!openOffer.isCanceled()) openOffer.setState(OpenOffer.State.PENDING); } @@ -1139,9 +1174,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe for (OpenOffer otherOffer : openOffers) { if (otherOffer == openOffer) continue; if (otherOffer.getState() != OpenOffer.State.PENDING) continue; - if (otherOffer.getScheduledTxHashes() == null) continue; - for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) { - if (txHash.equals(scheduledTxHash)) return true; + if (txHash.equals(otherOffer.getSplitOutputTxHash())) return true; + if (otherOffer.getScheduledTxHashes() != null) { + for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) { + if (txHash.equals(scheduledTxHash)) return true; + } } } return false; diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 27103cb6..3388368a 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -86,7 +86,7 @@ public class MakerReserveOfferFunds extends Task { //if (true) throw new RuntimeException("Pretend error"); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); } catch (Exception e) { - log.warn("Error creating reserve tx, attempt={}/{}, offerId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage()); + log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; model.getProtocol().startTimeoutTimer(); // reset protocol timeout if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection(); diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 4e3c38ae..1d8a32d3 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -476,7 +476,7 @@ public final class ArbitrationManager extends DisputeManager> exten }; errorMessageListener = (o, oldValue, newValue) -> { - if (newValue != null) + if (model.createOfferCanceled) return; + if (newValue != null) { UserThread.runAfter(() -> new Popup().error(Res.get("createOffer.amountPriceBox.error.message", model.errorMessage.get())) - .show(), 100, TimeUnit.MILLISECONDS); + .show(), 100, TimeUnit.MILLISECONDS); + } }; paymentAccountsComboBoxSelectionHandler = e -> onPaymentAccountsComboBoxSelected(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index fa0dfbbf..33233083 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -109,6 +109,7 @@ public abstract class MutableOfferViewModel ext private String addressAsString; private final String paymentLabel; private boolean createOfferRequested; + public boolean createOfferCanceled; public final StringProperty amount = new SimpleStringProperty(); public final StringProperty minAmount = new SimpleStringProperty(); @@ -608,6 +609,7 @@ public abstract class MutableOfferViewModel ext void onPlaceOffer(Offer offer, Runnable resultHandler) { errorMessage.set(null); createOfferRequested = true; + createOfferCanceled = false; dataModel.onPlaceOffer(offer, transaction -> { resultHandler.run(); @@ -631,6 +633,7 @@ public abstract class MutableOfferViewModel ext public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { createOfferRequested = false; + createOfferCanceled = true; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; Optional openOffer = openOfferManager.getOpenOfferById(offer.getId()); if (openOffer.isPresent()) { From b61f1fabcdce69bee6cc2c818dfc8235da98392e Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 20 Jul 2024 14:30:55 -0400 Subject: [PATCH 55/58] disable synchronizing on daemon for performance --- core/src/main/java/haveno/core/api/XmrConnectionService.java | 2 +- core/src/main/java/haveno/core/trade/HavenoUtils.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index caddf299..6f9b4bb3 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -273,7 +273,7 @@ public final class XmrConnectionService { } public synchronized boolean requestSwitchToNextBestConnection() { - log.warn("Request made to switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri()); + log.warn("Requesting switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri()); // skip if shut down started if (isShutDownStarted) { diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index ff8de230..bcb0d9f4 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -82,7 +82,7 @@ public class HavenoUtils { public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes // synchronize requests to the daemon - private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool) + private static boolean SYNC_DAEMON_REQUESTS = false; // sync long requests to daemon (e.g. refresh, update pool) // TODO: performance suffers by syncing daemon requests, but otherwise we sometimes get sporadic errors? private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create txs) private static Object DAEMON_LOCK = new Object(); public static Object getDaemonLock() { From 7c8753c17b210285692c0cacc4849a6e4fb0a586 Mon Sep 17 00:00:00 2001 From: PW Date: Sat, 20 Jul 2024 21:29:57 -0400 Subject: [PATCH 56/58] Make offers filterable via offer ID or Node address --- .../resources/i18n/displayStrings.properties | 3 ++ .../components/AutoTooltipTextField.java | 45 +++++++++++++++++++ .../main/offer/offerbook/OfferBookView.java | 14 +++++- .../offer/offerbook/OfferBookViewModel.java | 18 +++++++- .../java/haveno/desktop/util/FormBuilder.java | 15 +++++++ 5 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 desktop/src/main/java/haveno/desktop/components/AutoTooltipTextField.java diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f43f1a6d..9c2796c6 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -311,6 +311,9 @@ market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Trades +# OfferBookView +market.offerBook.filterPrompt=Offer ID, address... + # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Sell {0} to market.offerBook.buyOffersHeaderLabel=Buy {0} from diff --git a/desktop/src/main/java/haveno/desktop/components/AutoTooltipTextField.java b/desktop/src/main/java/haveno/desktop/components/AutoTooltipTextField.java new file mode 100644 index 00000000..1473ed09 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/components/AutoTooltipTextField.java @@ -0,0 +1,45 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package haveno.desktop.components; + +import com.jfoenix.controls.JFXTextField; +import com.jfoenix.skins.JFXTextFieldSkin; +import javafx.scene.control.Skin; +import javafx.scene.control.TextField; + +public class AutoTooltipTextField extends JFXTextField { + + public AutoTooltipTextField() { + super(); + } + + public AutoTooltipTextField(String text) { + super(text); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoTooltipTextFieldSkin(this); + } + + private class AutoTooltipTextFieldSkin extends JFXTextFieldSkin { + public AutoTooltipTextFieldSkin(TextField textField) { + super(textField); + } + } +} diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index ac10fd7d..f0333042 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -46,6 +46,7 @@ import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipTableColumn; +import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.ColoredDecimalPlacesWithZerosText; import haveno.desktop.components.HyperlinkWithIcon; @@ -107,6 +108,7 @@ import java.util.Map; import java.util.Optional; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; +import static haveno.desktop.util.FormBuilder.addTopLabelAutoToolTipTextField; abstract public class OfferBookView extends ActivatableViewAndModel { @@ -122,6 +124,7 @@ abstract public class OfferBookView currencyComboBox; private AutocompleteComboBox paymentMethodComboBox; private AutoTooltipButton createOfferButton; + private AutoTooltipTextField filterInputField; private AutoTooltipSlideToggleButton matchingOffersToggle; private AutoTooltipTableColumn amountColumn; private AutoTooltipTableColumn volumeColumn; @@ -212,8 +215,13 @@ abstract public class OfferBookView autoToolTipTextField = addTopLabelAutoToolTipTextField(Res.get("shared.filter")); + VBox filterBox = autoToolTipTextField.first; + filterInputField = autoToolTipTextField.third; + filterInputField.setPromptText(Res.get("market.offerBook.filterPrompt")); + offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, - matchingOffersToggle, getSpacer(), createOfferButtonStack); + filterBox, matchingOffersToggle, getSpacer(), createOfferButtonStack); GridPane.setHgrow(offerToolsBox, Priority.ALWAYS); GridPane.setRowIndex(offerToolsBox, gridRow); @@ -427,6 +435,10 @@ abstract public class OfferBookView { + model.onFilterKeyTyped(filterInputField.getText()); + }); } private void updatePaymentMethodComboBoxEditor() { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index 23446012..37e9f0a4 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -121,6 +121,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod(); boolean isTabSelected; + String filterText = ""; final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(true); final BooleanProperty disableMatchToggle = new SimpleBooleanProperty(); final IntegerProperty maxPlacesForAmount = new SimpleIntegerProperty(); @@ -269,6 +270,11 @@ abstract class OfferBookViewModel extends ActivatableViewModel { } } + void onFilterKeyTyped(String filterText) { + this.filterText = filterText; + filterOffers(); + } + abstract void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code); protected void onSetPaymentMethod(PaymentMethod paymentMethod) { @@ -566,7 +572,17 @@ abstract class OfferBookViewModel extends ActivatableViewModel { Predicate predicate = useOffersMatchingMyAccountsFilter ? getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) : getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); - filteredItems.setPredicate(predicate); + + if (!filterText.isEmpty()) { + Predicate nextPredicate=offerBookListItem -> + offerBookListItem.getOffer().getOfferPayload().getOwnerNodeAddress().getFullAddress().contains(filterText); + nextPredicate=nextPredicate.or(offerBookListItem -> + offerBookListItem.getOffer().getId().contains(filterText)); + + filteredItems.setPredicate(predicate.and(nextPredicate)); + } else { + filteredItems.setPredicate(predicate); + } } abstract Predicate getCurrencyAndMethodPredicate(OfferDirection direction, diff --git a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java index cae26b80..4181245d 100644 --- a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java @@ -36,6 +36,7 @@ import haveno.desktop.components.AutoTooltipCheckBox; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipRadioButton; import haveno.desktop.components.AutoTooltipSlideToggleButton; +import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; @@ -1286,6 +1287,20 @@ public class FormBuilder { return new Tuple3<>(vBox, label, comboBox); } + public static Tuple3 addTopLabelAutoToolTipTextField(String title) { + return addTopLabelAutoToolTipTextField(title, 0); + } + + public static Tuple3 addTopLabelAutoToolTipTextField(String title, int top) { + Label label = getTopLabel(title); + VBox vBox = getTopLabelVBox(top); + + final AutoTooltipTextField textField = new AutoTooltipTextField(); + vBox.getChildren().addAll(label, textField); + + return new Tuple3<>(vBox, label, textField); + } + @NotNull private static VBox getTopLabelVBox(int top) { VBox vBox = new VBox(); From b03c873a06f0f3c9ed368a4e33beebbdee33ed84 Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 20 Jul 2024 21:39:10 -0400 Subject: [PATCH 57/58] search by payment method in filter and tweak ui --- .../resources/i18n/displayStrings.properties | 2 +- .../main/offer/offerbook/OfferBookView.java | 6 +++--- .../main/offer/offerbook/OfferBookViewModel.java | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 9c2796c6..a38f13bf 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -312,7 +312,7 @@ market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Trades # OfferBookView -market.offerBook.filterPrompt=Offer ID, address... +market.offerBook.filterPrompt=Filter # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Sell {0} to diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index f0333042..ead182ba 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -188,13 +188,13 @@ abstract public class OfferBookView> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByPaymentMethod")); paymentMethodComboBox = paymentBoxTuple.third; paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); - paymentMethodComboBox.setPrefWidth(270); + paymentMethodComboBox.setPrefWidth(250); matchingOffersToggle = new AutoTooltipSlideToggleButton(); matchingOffersToggle.setText(Res.get("offerbook.matchingOffers")); @@ -215,7 +215,7 @@ abstract public class OfferBookView autoToolTipTextField = addTopLabelAutoToolTipTextField(Res.get("shared.filter")); + Tuple3 autoToolTipTextField = addTopLabelAutoToolTipTextField(""); VBox filterBox = autoToolTipTextField.first; filterInputField = autoToolTipTextField.third; filterInputField.setPromptText(Res.get("market.offerBook.filterPrompt")); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index 37e9f0a4..f5fdd745 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -574,10 +574,18 @@ abstract class OfferBookViewModel extends ActivatableViewModel { getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); if (!filterText.isEmpty()) { - Predicate nextPredicate=offerBookListItem -> - offerBookListItem.getOffer().getOfferPayload().getOwnerNodeAddress().getFullAddress().contains(filterText); - nextPredicate=nextPredicate.or(offerBookListItem -> - offerBookListItem.getOffer().getId().contains(filterText)); + + // filter node address + Predicate nextPredicate = offerBookListItem -> + offerBookListItem.getOffer().getOfferPayload().getOwnerNodeAddress().getFullAddress().toLowerCase().contains(filterText.toLowerCase()); + + // filter offer id + nextPredicate = nextPredicate.or(offerBookListItem -> + offerBookListItem.getOffer().getId().toLowerCase().contains(filterText.toLowerCase())); + + // filter payment method + nextPredicate = nextPredicate.or(offerBookListItem -> + Res.get(offerBookListItem.getOffer().getPaymentMethod().getId()).toLowerCase().contains(filterText.toLowerCase())); filteredItems.setPredicate(predicate.and(nextPredicate)); } else { From 629e1508f2251d7d2fccb94d06d72d0be4e0be0d Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 20 Jul 2024 21:41:20 -0400 Subject: [PATCH 58/58] do not request connection change on trade poll error except rescan --- core/src/main/java/haveno/core/trade/Trade.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 973cf7e2..0f629136 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2546,7 +2546,12 @@ public abstract class Trade implements Tradable, Model { // rescan spent outputs to detect unconfirmed payout tx if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { - wallet.rescanSpent(); + try { + wallet.rescanSpent(); + } catch (Exception e) { + log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); // do not block polling thread + } } // get txs from trade wallet @@ -2596,7 +2601,6 @@ public abstract class Trade implements Tradable, Model { boolean isWalletConnected = isWalletConnectedToDaemon(); if (wallet != null && !isShutDownStarted && isWalletConnected) { log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); - ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); // do not block polling thread //e.printStackTrace(); } }