From 3dde7c6261971aa1174e7f7ebd19fb81a5622938 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Wed, 18 Jan 2023 08:27:46 -0600 Subject: [PATCH] Refactor closing ticket logic to fix #6517. --- .../support/dispute/refund/RefundManager.java | 8 ++ .../windows/DisputeSummaryWindow.java | 132 +++++++++++------- 2 files changed, 91 insertions(+), 49 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 0ec75e3277..346a0dc5b2 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -264,6 +264,14 @@ public final class RefundManager extends DisputeManager { String takerFeeTxId, String depositTxId, String delayedPayoutTxId) { + // in regtest mode, simulate a delay & failure obtaining the blockchain transactions + // since we cannot request them in regtest anyway. this is useful for checking failure scenarios + if (!Config.baseCurrencyNetwork().isMainnet()) { + CompletableFuture> retFuture = new CompletableFuture<>(); + UserThread.runAfter(() -> retFuture.complete(new ArrayList<>()), 5); + return retFuture; + } + NetworkParameters params = btcWalletService.getParams(); List txs = new ArrayList<>(); return mempoolService.requestTxAsHex(makerFeeTxId) diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 55cfebac1d..56e59948c5 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -55,8 +55,6 @@ import bisq.core.util.coin.CoinUtil; import bisq.common.UserThread; import bisq.common.app.DevEnv; -import bisq.common.config.Config; -import bisq.common.handlers.ResultHandler; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; @@ -91,6 +89,7 @@ import java.time.Instant; import java.util.Date; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; @@ -287,7 +286,7 @@ public class DisputeSummaryWindow extends Overlay { setReasonRadioButtonState(); addSummaryNotes(); - addButtons(contract); + addButtons(); } private void addInfoPane() { @@ -674,7 +673,7 @@ public class DisputeSummaryWindow extends Overlay { summaryNotesTextArea.textProperty().bindBidirectional(disputeResult.summaryNotesProperty()); } - private void addButtons(Contract contract) { + protected void addButtons() { Tuple3 tuple = add2ButtonsWithBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.close.button"), Res.get("shared.cancel"), 15, true); @@ -692,21 +691,29 @@ public class DisputeSummaryWindow extends Overlay { Button cancelButton = tuple.second; closeTicketButton.setOnAction(e -> { - if (dispute.getDepositTxSerialized() == null) { - log.warn("dispute.getDepositTxSerialized is null"); - return; - } + if (dispute.getDepositTxSerialized() == null) { + log.warn("dispute.getDepositTxSerialized is null"); + return; + } - if (dispute.getSupportType() == SupportType.REFUND && - peersDisputeOptional.isPresent() && - !peersDisputeOptional.get().isClosed()) { - showPayoutTxConfirmation(contract, - disputeResult, - () -> doCloseAfterTxsVerified(closeTicketButton)); - } else { - doCloseAfterTxsVerified(closeTicketButton); - } - }); + if (peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed()) { + closeTicket(closeTicketButton); // all checks done already on peers ticket + } else { + maybeCheckTransactions().thenAccept(continue1 -> { + if (continue1) { + checkGeneralValidity().thenAccept(continue2 -> { + if (continue2) { + maybeMakePayout().thenAccept(continue3 -> { + if (continue3) { + closeTicket(closeTicketButton); + } + }); + } + }); + } + }); + } + }); cancelButton.setOnAction(e -> { dispute.setDisputeResult(disputeResult); @@ -715,21 +722,34 @@ public class DisputeSummaryWindow extends Overlay { }); } - private void showPayoutTxConfirmation(Contract contract, DisputeResult disputeResult, ResultHandler resultHandler) { + private CompletableFuture maybeMakePayout() { + final CompletableFuture asyncStatus = new CompletableFuture<>(); + + // bypass for mediation tickets, or when the peer's ticket is already closed + if (dispute.getSupportType() == SupportType.MEDIATION || + (peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed())) { + asyncStatus.complete(true); + return asyncStatus; + } + if (dispute.isPayoutDone()) { new Popup().headLine(Res.get("disputeSummaryWindow.close.alreadyPaid.headline")) .confirmation(Res.get("disputeSummaryWindow.close.alreadyPaid.text")) .closeButtonText(Res.get("shared.cancel")) + .actionButtonText("Close ticket") + .onAction(() -> asyncStatus.complete(true)) .show(); + return asyncStatus; } if (payoutPromptOnDisplay != null) { log.warn("The payout prompt is already on display, we do not show another copy of it."); - return; + asyncStatus.complete(false); + return asyncStatus; } Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); - String buyerPayoutAddressString = contract.getBuyerPayoutAddressString(); + String buyerPayoutAddressString = dispute.getContract().getBuyerPayoutAddressString(); Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); - String sellerPayoutAddressString = contract.getSellerPayoutAddressString(); + String sellerPayoutAddressString = dispute.getContract().getSellerPayoutAddressString(); Coin outputAmount = buyerPayoutAmount.add(sellerPayoutAmount); Tuple2 feeTuple = txFeeEstimationService.getEstimatedFeeAndTxVsize(outputAmount, btcWalletService); Coin fee = feeTuple.first; @@ -767,11 +787,14 @@ public class DisputeSummaryWindow extends Overlay { fee, buyerPayoutAddressString, sellerPayoutAddressString, - resultHandler); + asyncStatus); }) + .secondaryActionButtonText("skip payout") + .onSecondaryAction(() -> asyncStatus.complete(true)) .closeButtonText(Res.get("shared.cancel")) .onClose(() -> { payoutPromptOnDisplay = null; + asyncStatus.complete(false); }) .show(); } else { @@ -779,10 +802,12 @@ public class DisputeSummaryWindow extends Overlay { new Popup().headLine(Res.get("disputeSummaryWindow.close.noPayout.headline")) .confirmation(Res.get("disputeSummaryWindow.close.noPayout.text")) .actionButtonText(Res.get("shared.yes")) - .onAction(resultHandler::handleResult) .closeButtonText(Res.get("shared.cancel")) + .onAction(() -> asyncStatus.complete(true)) + .onClose(() -> asyncStatus.complete(false)) .show(); } + return asyncStatus; } private void doPayout(Coin buyerPayoutAmount, @@ -790,10 +815,11 @@ public class DisputeSummaryWindow extends Overlay { Coin fee, String buyerPayoutAddressString, String sellerPayoutAddressString, - ResultHandler resultHandler) { + CompletableFuture resultHandler) { if (dispute.isPayoutDone()) { log.error("Payout already processed, returning to avoid double payout for dispute of trade {}", dispute.getTradeId()); + resultHandler.complete(true); return; } dispute.setPayoutDone(true); @@ -806,32 +832,33 @@ public class DisputeSummaryWindow extends Overlay { tradeWalletService.broadcastTx(tx, new TxBroadcaster.Callback() { @Override public void onSuccess(Transaction transaction) { - resultHandler.handleResult(); + resultHandler.complete(true); } @Override public void onFailure(TxBroadcastException exception) { log.error("TxBroadcastException at doPayout", exception); - new Popup().error(exception.toString()).show(); + new Popup().error(exception.toString()).onClose(() -> resultHandler.complete(false)).show(); } }); } catch (InsufficientMoneyException | WalletException | TransactionVerificationException e) { log.error("Exception at doPayout", e); - new Popup().error(e.toString()).show(); + new Popup().error(e.toString()).onClose(() -> resultHandler.complete(false)).show(); } } - private void doCloseAfterTxsVerified(Button closeTicketButton) { + private CompletableFuture maybeCheckTransactions() { + final CompletableFuture asyncStatus = new CompletableFuture<>(); var disputeManager = getDisputeManager(dispute); // Only RefundAgent need to verify transactions to ensure payout is safe - if (disputeManager instanceof RefundManager && Config.baseCurrencyNetwork().isMainnet()) { + if (disputeManager instanceof RefundManager) { RefundManager refundManager = (RefundManager) disputeManager; Contract contract = dispute.getContract(); String makerFeeTxId = contract.getOfferPayload().getOfferFeePaymentTxId(); String takerFeeTxId = contract.getTakerFeeTxID(); String depositTxId = dispute.getDepositTxId(); String delayedPayoutTxId = dispute.getDelayedPayoutTxId(); - Popup requestingTxsPopup = new Popup().feedback(Res.get("disputeSummaryWindow.requestingTxs")); + Popup requestingTxsPopup = new Popup().information(Res.get("disputeSummaryWindow.requestingTxs")).hideCloseButton(); requestingTxsPopup.show(); refundManager.requestBlockchainTransactions(makerFeeTxId, takerFeeTxId, @@ -844,25 +871,28 @@ public class DisputeSummaryWindow extends Overlay { if (throwable == null) { try { refundManager.verifyTradeTxChain(txList); - if (!dispute.isUsingLegacyBurningMan()) { Transaction delayedPayoutTx = txList.get(3); refundManager.verifyDelayedPayoutTxReceivers(delayedPayoutTx, dispute); - doCloseIfValid(closeTicketButton); - } else { - doCloseIfValid(closeTicketButton); } + asyncStatus.complete(true); } catch (Throwable error) { - UserThread.runAfter(() -> - new Popup().warning(Res.get("disputeSummaryWindow.delayedPayoutTxVerificationFailed", error.getMessage())) - .show(), + UserThread.runAfter(() -> { + Popup popup = new Popup(); + popup.warning(Res.get("disputeSummaryWindow.delayedPayoutTxVerificationFailed", error.getMessage())) + .actionButtonText(Res.get("shared.continueAnyway")) + .onAction(() -> asyncStatus.complete(true)) + .onClose(() -> asyncStatus.complete(false)) + .show(); + }, 100, TimeUnit.MILLISECONDS); } } else { UserThread.runAfter(() -> new Popup().warning(Res.get("disputeSummaryWindow.requestTransactionsError", throwable.getMessage())) - .onAction(() -> doCloseIfValid(closeTicketButton)) + .onAction(() -> asyncStatus.complete(true)) + .onClose(() -> asyncStatus.complete(false)) .show(), 100, TimeUnit.MILLISECONDS); @@ -870,18 +900,21 @@ public class DisputeSummaryWindow extends Overlay { }); }); } else { - doCloseIfValid(closeTicketButton); + asyncStatus.complete(true); } + return asyncStatus; } - private void doCloseIfValid(Button closeTicketButton) { + + private CompletableFuture checkGeneralValidity() { + final CompletableFuture asyncStatus = new CompletableFuture<>(); var disputeManager = checkNotNull(getDisputeManager(dispute)); try { DisputeValidation.testIfDisputeTriesReplay(dispute, disputeManager.getDisputesAsObservableList()); if (dispute.isUsingLegacyBurningMan()) { DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade); } - doClose(closeTicketButton); + asyncStatus.complete(true); } catch (DisputeValidation.AddressException exception) { String addressAsString = dispute.getDonationAddressOfDelayedPayoutTx(); String tradeId = dispute.getTradeId(); @@ -895,11 +928,10 @@ public class DisputeSummaryWindow extends Overlay { daoFacade.getAllDonationAddresses(), tradeId, Res.get("support.warning.disputesWithInvalidDonationAddress.mediator"))) - .onAction(() -> { - doClose(closeTicketButton); - }) .actionButtonText(Res.get("shared.yes")) .closeButtonText(Res.get("shared.no")) + .onAction(() -> asyncStatus.complete(true)) + .onClose(() -> asyncStatus.complete(false)) .show(); } else { new Popup().width(900) @@ -908,6 +940,7 @@ public class DisputeSummaryWindow extends Overlay { daoFacade.getAllDonationAddresses(), tradeId, Res.get("support.warning.disputesWithInvalidDonationAddress.refundAgent"))) + .onClose(() -> asyncStatus.complete(false)) .show(); } } catch (DisputeValidation.DisputeReplayException exception) { @@ -915,22 +948,23 @@ public class DisputeSummaryWindow extends Overlay { log.error("Closing of ticket failed as mediator", exception); new Popup().width(900) .warning(exception.getMessage()) - .onAction(() -> { - doClose(closeTicketButton); - }) .actionButtonText(Res.get("shared.yes")) .closeButtonText(Res.get("shared.no")) + .onAction(() -> asyncStatus.complete(true)) + .onClose(() -> asyncStatus.complete(false)) .show(); } else { log.error("Closing of ticket failed", exception); new Popup().width(900) .warning(exception.getMessage()) + .onAction(() -> asyncStatus.complete(false)) .show(); } } + return asyncStatus; } - private void doClose(Button closeTicketButton) { + private void closeTicket(Button closeTicketButton) { DisputeManager> disputeManager = getDisputeManager(dispute); if (disputeManager == null) { return;