From c8f347d56896a36caad98db2e4e2be6de97b862d Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:45:14 -0400 Subject: [PATCH 1/3] skip connection switch on illegal error syncing wallet --- core/src/main/java/haveno/core/trade/Trade.java | 6 ++++-- 1 file changed, 4 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 9772a71f7c9..9c3d0a5b800 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2581,7 +2581,7 @@ private void syncWallet(boolean pollWallet) { MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { synchronized (walletLock) { - if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); + if (getWallet() == null) throw new IllegalStateException("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()); @@ -2601,7 +2601,9 @@ private void syncWallet(boolean pollWallet) { if (pollWallet) doPollWallet(); } catch (Exception e) { - if (!isShutDownStarted) ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); + if (!(e instanceof IllegalStateException) && !isShutDownStarted) { + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); + } throw e; } } From c10ae90985f5f39e974d40611df1632e3c8e027d Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 26 Jul 2025 11:18:35 -0400 Subject: [PATCH 2/3] request connection switch every other attempt --- .../java/haveno/core/offer/OpenOfferManager.java | 2 +- .../placeoffer/tasks/MakerReserveOfferFunds.java | 2 +- core/src/main/java/haveno/core/trade/Trade.java | 12 ++++++------ .../haveno/core/trade/protocol/TradeProtocol.java | 1 + .../trade/protocol/tasks/MakerRecreateReserveTx.java | 2 +- .../protocol/tasks/MaybeSendSignContractRequest.java | 2 +- .../trade/protocol/tasks/TakerReserveTradeFunds.java | 2 +- .../haveno/core/xmr/wallet/XmrWalletService.java | 5 +++-- 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index e4895fbe075..46ff7c1f4e4 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1358,7 +1358,7 @@ private MoneroTxWallet splitAndSchedule(OpenOffer openOffer) { } catch (Exception e) { 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()); - xmrWalletService.handleWalletError(e, sourceConnection); + xmrWalletService.handleWalletError(e, sourceConnection, i + 1); if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; 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 88001725052..c0dd076232f 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 @@ -100,7 +100,7 @@ protected void run() { throw e; } catch (Exception e) { log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - model.getXmrWalletService().handleWalletError(e, sourceConnection); + model.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1); verifyPending(); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; model.getProtocol().startTimeoutTimer(); // reset protocol timeout diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 9c3d0a5b800..9a53bfaf08d 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1152,7 +1152,7 @@ public void importMultisigHex() { throw e; } catch (Exception e) { log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - handleWalletError(e, sourceConnection); + handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; @@ -1237,9 +1237,9 @@ private void doImportMultisigHex() { log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); } - private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) { + private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection, int numAttempts) { if (HavenoUtils.isUnresponsive(e)) forceCloseWallet(); // wallet can be stuck a while - if (!HavenoUtils.isIllegal(e) && xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); + if (numAttempts % TradeProtocol.REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS == 0 && !HavenoUtils.isIllegal(e) && xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); // request connection switch every n attempts getWallet(); // re-open wallet } @@ -1275,7 +1275,7 @@ public MoneroTxWallet createPayoutTx() { } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { - handleWalletError(e, sourceConnection); + handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); @@ -1337,7 +1337,7 @@ public MoneroTxWallet createDisputePayoutTx(MoneroTxConfig txConfig) { throw e; } catch (Exception e) { if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee"); - handleWalletError(e, sourceConnection); + handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); @@ -1368,7 +1368,7 @@ public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { - handleWalletError(e, sourceConnection); + handleWalletError(e, sourceConnection, i + 1); doPollWallet(); if (isPayoutPublished()) break; log.warn("Failed to process payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e); 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 4249e967a97..1051b7b2b32 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -101,6 +101,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D public static final int TRADE_STEP_TIMEOUT_SECONDS = Config.baseCurrencyNetwork().isTestnet() ? 60 : 180; private static final String TIMEOUT_REACHED = "Timeout reached."; public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions + public static final int REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS = 2; // request connection switch on even attempts public static final long REPROCESS_DELAY_MS = 5000; public static final String LOG_HIGHLIGHT = ""; // TODO: how to highlight some logs with cyan? ("\u001B[36m")? coloring works in the terminal but prints character literals to .log files diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java index 6ad3ed863e6..cee6e0d9c66 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerRecreateReserveTx.java @@ -98,7 +98,7 @@ protected void run() { throw e; } catch (Exception e) { log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying 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 b9138fdbc2e..5ab9a754944 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 @@ -113,7 +113,7 @@ protected void run() { depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); } catch (Exception e) { log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; 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 555a558bd58..976e7a4fdc7 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 @@ -75,7 +75,7 @@ protected void run() { throw e; } catch (Exception e) { log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - trade.getXmrWalletService().handleWalletError(e, sourceConnection); + trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying 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 f805818d55d..9a3cac50bea 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -37,6 +37,7 @@ import haveno.core.trade.MakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; +import haveno.core.trade.protocol.TradeProtocol; import haveno.core.user.Preferences; import haveno.core.user.User; import haveno.core.xmr.listeners.XmrBalanceListener; @@ -1944,9 +1945,9 @@ public void forceRestartMainWallet() { doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); } - public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) { + public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection, int numAttempts) { if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while - requestSwitchToNextBestConnection(sourceConnection); + if (numAttempts % TradeProtocol.REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS == 0) requestSwitchToNextBestConnection(sourceConnection); // request connection switch every n attempts if (wallet == null) doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); } From 293c41dd151b48163b91e2e684ce8a57a788c90f Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 28 Jul 2025 09:52:13 -0400 Subject: [PATCH 3/3] improve error handling on create dispute payout tx --- .../core/support/dispute/DisputeManager.java | 112 +++++++++--------- .../windows/DisputeSummaryWindow.java | 85 +++++++------ 2 files changed, 109 insertions(+), 88 deletions(-) diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index 5d5cc840402..6fc308f9fc5 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -497,11 +497,11 @@ protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) { // get trade Trade trade = tradeManager.getTrade(msgDispute.getTradeId()); if (trade == null) { - log.warn("Dispute trade {} does not exist", msgDispute.getTradeId()); + log.warn("Ignoring DisputeOpenedMessage for trade {} because it does not exist", msgDispute.getTradeId()); return; } if (trade.isPayoutPublished()) { - log.warn("Dispute trade {} payout already published", msgDispute.getTradeId()); + log.warn("Ignoring DisputeOpenedMessage for {} {} because payout is already published", trade.getClass().getSimpleName(), trade.getId()); return; } @@ -934,66 +934,70 @@ public MoneroTxWallet createDisputePayoutTx(Trade trade, Contract contract, Disp // sync and poll trade.syncAndPollWallet(); - // create unsigned dispute payout tx if not already published - if (!trade.isPayoutPublished()) { + // check if payout tx already published + String alreadyPublishedMsg = "Cannot create dispute payout tx because payout tx is already published for trade " + trade.getId(); + if (trade.isPayoutPublished()) throw new RuntimeException(alreadyPublishedMsg); - // create unsigned dispute payout tx - if (updateState) log.info("Creating unsigned dispute payout tx for trade {}", trade.getId()); - try { + // create unsigned dispute payout tx + if (updateState) log.info("Creating unsigned dispute payout tx for trade {}", trade.getId()); + try { - // trade wallet must be synced - if (trade.getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + trade.getId()); + // trade wallet must be synced + if (trade.getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + trade.getId()); - // check amounts - if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative"); - if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative"); - if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).compareTo(trade.getWallet().getUnlockedBalance()) > 0) { - throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()))); - } + // check amounts + if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative"); + if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative"); + if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).compareTo(trade.getWallet().getUnlockedBalance()) > 0) { + throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()))); + } - // create dispute payout tx config - MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0); - String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString(); - String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); - txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); - if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost()); - if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost()); - - // configure who pays mining fee - BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost(); - if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0 - else { - switch (disputeResult.getSubtractFeeFrom()) { - case BUYER_AND_SELLER: - txConfig.setSubtractFeeFrom(0, 1); - break; - case BUYER_ONLY: - txConfig.setSubtractFeeFrom(0); - break; - case SELLER_ONLY: - txConfig.setSubtractFeeFrom(1); - break; - } + // create dispute payout tx config + MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0); + String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString(); + String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); + txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); + if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost()); + if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost()); + + // configure who pays mining fee + BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost(); + if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0 + else { + switch (disputeResult.getSubtractFeeFrom()) { + case BUYER_AND_SELLER: + txConfig.setSubtractFeeFrom(0, 1); + break; + case BUYER_ONLY: + txConfig.setSubtractFeeFrom(0); + break; + case SELLER_ONLY: + txConfig.setSubtractFeeFrom(1); + break; } + } - // create dispute payout tx - MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig); - - // update trade state - if (updateState) { - trade.getProcessModel().setUnsignedPayoutTx(payoutTx); - trade.updatePayout(payoutTx); - if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); - if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); - } - trade.requestPersistence(); - return payoutTx; - } catch (Exception e) { - trade.syncAndPollWallet(); - if (!trade.isPayoutPublished()) throw e; + // create dispute payout tx + MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig); + + // update trade state + if (updateState) { + trade.getProcessModel().setUnsignedPayoutTx(payoutTx); + trade.updatePayout(payoutTx); + if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); } + trade.requestPersistence(); + return payoutTx; + } catch (Exception e) { + trade.syncAndPollWallet(); + if (trade.isPayoutPublished()) throw new IllegalStateException(alreadyPublishedMsg); + throw e; + } catch (AssertionError e) { // tx creation throws assertion error with invalid config + trade.syncAndPollWallet(); + if (trade.isPayoutPublished()) throw new IllegalStateException(alreadyPublishedMsg); + throw new RuntimeException(e); } - return null; // can be null if already published or we don't have receiver's multisig hex } private Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index dc64af37d58..a90395907f4 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -220,31 +220,11 @@ private void addContent() { disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); disputeResult.setSubtractFeeFrom(peersDisputeResult.getSubtractFeeFrom()); - buyerGetsTradeAmountRadioButton.setDisable(true); - buyerGetsAllRadioButton.setDisable(true); - sellerGetsTradeAmountRadioButton.setDisable(true); - sellerGetsAllRadioButton.setDisable(true); - customRadioButton.setDisable(true); - - buyerPayoutAmountInputTextField.setDisable(true); - sellerPayoutAmountInputTextField.setDisable(true); - buyerPayoutAmountInputTextField.setEditable(false); - sellerPayoutAmountInputTextField.setEditable(false); - - reasonWasBugRadioButton.setDisable(true); - reasonWasUsabilityIssueRadioButton.setDisable(true); - reasonProtocolViolationRadioButton.setDisable(true); - reasonNoReplyRadioButton.setDisable(true); - reasonWasScamRadioButton.setDisable(true); - reasonWasOtherRadioButton.setDisable(true); - reasonWasBankRadioButton.setDisable(true); - reasonWasOptionTradeRadioButton.setDisable(true); - reasonWasSellerNotRespondingRadioButton.setDisable(true); - reasonWasWrongSenderAccountRadioButton.setDisable(true); - reasonWasPeerWasLateRadioButton.setDisable(true); - reasonWasTradeAlreadySettledRadioButton.setDisable(true); - + disableTradeAmountPayoutControls(); applyTradeAmountRadioButtonStates(); + } else if (trade.isPayoutPublished()) { + log.warn("Payout is already published for {} {}, disabling payout controls", trade.getClass().getSimpleName(), trade.getId()); + disableTradeAmountPayoutControls(); } setReasonRadioButtonState(); @@ -253,6 +233,32 @@ private void addContent() { addButtons(contract); } + private void disableTradeAmountPayoutControls() { + buyerGetsTradeAmountRadioButton.setDisable(true); + buyerGetsAllRadioButton.setDisable(true); + sellerGetsTradeAmountRadioButton.setDisable(true); + sellerGetsAllRadioButton.setDisable(true); + customRadioButton.setDisable(true); + + buyerPayoutAmountInputTextField.setDisable(true); + sellerPayoutAmountInputTextField.setDisable(true); + buyerPayoutAmountInputTextField.setEditable(false); + sellerPayoutAmountInputTextField.setEditable(false); + + reasonWasBugRadioButton.setDisable(true); + reasonWasUsabilityIssueRadioButton.setDisable(true); + reasonProtocolViolationRadioButton.setDisable(true); + reasonNoReplyRadioButton.setDisable(true); + reasonWasScamRadioButton.setDisable(true); + reasonWasOtherRadioButton.setDisable(true); + reasonWasBankRadioButton.setDisable(true); + reasonWasOptionTradeRadioButton.setDisable(true); + reasonWasSellerNotRespondingRadioButton.setDisable(true); + reasonWasWrongSenderAccountRadioButton.setDisable(true); + reasonWasPeerWasLateRadioButton.setDisable(true); + reasonWasTradeAlreadySettledRadioButton.setDisable(true); + } + private void addInfoPane() { Contract contract = dispute.getContract(); addTitledGroupBg(gridPane, ++rowIndex, 17, Res.get("disputeSummaryWindow.title")).getStyleClass().add("last"); @@ -581,16 +587,27 @@ private void addButtons(Contract contract) { !trade.isPayoutPublished()) { // create payout tx - MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); - - // show confirmation - showPayoutTxConfirmation(contract, - payoutTx, - () -> doClose(closeTicketButton, cancelButton), - () -> { - closeTicketButton.setDisable(false); - cancelButton.setDisable(false); - }); + try { + MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); + + // show confirmation + showPayoutTxConfirmation(contract, + payoutTx, + () -> doClose(closeTicketButton, cancelButton), + () -> { + closeTicketButton.setDisable(false); + cancelButton.setDisable(false); + }); + } catch (Exception ex) { + if (trade.isPayoutPublished()) { + doClose(closeTicketButton, cancelButton); + } else { + log.error("Error creating dispute payout tx for dispute: " + ex.getMessage(), ex); + new Popup().error(ex.getMessage()).show(); + closeTicketButton.setDisable(false); + cancelButton.setDisable(false); + } + } } else { doClose(closeTicketButton, cancelButton); }