From d20ad82a9fc91862477db596ac1f314a9784be40 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] improve error handling on create dispute payout tx --- .../core/support/dispute/DisputeManager.java | 118 +++++++++--------- .../windows/DisputeSummaryWindow.java | 83 +++++++----- 2 files changed, 111 insertions(+), 90 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 5d5cc84040..6fc308f9fc 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 @@ public abstract class DisputeManager> extends Sup // 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 abstract class DisputeManager> extends Sup // 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()))); - } - - // 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; + // 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 + 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 dc64af37d5..a90395907f 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 @@ public class DisputeSummaryWindow extends Overlay { 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 @@ public class DisputeSummaryWindow extends Overlay { 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 @@ public class DisputeSummaryWindow extends Overlay { !trade.isPayoutPublished()) { // create payout tx - MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); + try { + MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true); - // show confirmation - showPayoutTxConfirmation(contract, - payoutTx, - () -> doClose(closeTicketButton, cancelButton), - () -> { - closeTicketButton.setDisable(false); - cancelButton.setDisable(false); - }); + // 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); }