From 360b8a4acea705e5cc848377bc9efca3ec6cffb4 Mon Sep 17 00:00:00 2001 From: Steven Barclay Date: Wed, 9 Oct 2024 22:51:23 +0800 Subject: [PATCH] Adapt refund agent tx chain validation to v5 protocol Make sure the logic to fetch the chain of trade txs from mempool.space, and validate the receiver list, works in the case of the v5 protocol. In that case, the published warning & redirect txs replace the DPT, giving five txs in the chain instead of four. Ensure that in both cases the number of confirmations of the last tx (DPT for v4, redirect tx for v5) shows up in Dispute Summary window (outside of regtest). Also fix a bug validating the outputs of the last tx, where it failed to iterate through the receiver tuples correctly, which would have prevented it from ever working in production (as there's more than one receiver). Finally, make the dispute validation more strict about which txId fields it allows to be null vs non-null, so that the DPT ID is always null when the redirect or warning txId fields are set (signifying a v5 protocol trade for refund disputes). Disallow use of the legacy BM in that case as well, for safety, as it should never be used for new trades. --- .../support/dispute/DisputeValidation.java | 54 ++++---- .../support/dispute/refund/RefundManager.java | 124 +++++++++++------ .../resources/i18n/displayStrings.properties | 4 +- .../windows/DisputeSummaryWindow.java | 129 ++++++++++-------- 4 files changed, 192 insertions(+), 119 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java index 14b06dcf17..581495d6c2 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java @@ -104,7 +104,12 @@ public static void validateTradeAndDispute(Dispute dispute, Trade trade, BtcWall checkArgument(dispute.getContract().equals(trade.getContract()), "contract must match contract from trade"); - if (trade.hasV5Protocol()) { + if (!trade.hasV5Protocol()) { + checkNotNull(trade.getDelayedPayoutTx(), "trade.getDelayedPayoutTx() must not be null"); + checkNotNull(dispute.getDelayedPayoutTxId(), "delayedPayoutTxId must not be null"); + checkArgument(dispute.getDelayedPayoutTxId().equals(trade.getDelayedPayoutTx().getTxId().toString()), + "delayedPayoutTxId must match delayedPayoutTxId from trade"); + } else if (dispute.getSupportType() == SupportType.REFUND) { String buyersWarningTxId = toTxId(trade.getBuyersWarningTx(btcWalletService)); String sellersWarningTxId = toTxId(trade.getSellersWarningTx(btcWalletService)); String buyersRedirectTxId = toTxId(trade.getBuyersRedirectTx(btcWalletService)); @@ -123,15 +128,12 @@ public static void validateTradeAndDispute(Dispute dispute, Trade trade, BtcWall checkArgument(isBuyerRedirect, "seller's redirectTx must be used with buyer's warningTx"); } } else { - checkNotNull(trade.getDelayedPayoutTx(), "trade.getDelayedPayoutTx() must not be null"); - checkNotNull(dispute.getDelayedPayoutTxId(), "delayedPayoutTxId must not be null"); - checkArgument(dispute.getDelayedPayoutTxId().equals(trade.getDelayedPayoutTx().getTxId().toString()), - "delayedPayoutTxId must match delayedPayoutTxId from trade"); + checkArgument(dispute.getDelayedPayoutTxId() == null, "delayedPayoutTxId should be null"); } + checkTxIdFieldCombination(dispute); checkNotNull(trade.getDepositTx(), "trade.getDepositTx() must not be null"); - checkNotNull(dispute.getDepositTxId(), "depositTxId must not be null"); - checkArgument(dispute.getDepositTxId().equals(trade.getDepositTx().getTxId().toString()), + checkArgument(trade.getDepositTx().getTxId().toString().equals(dispute.getDepositTxId()), "depositTx must match depositTx from trade"); checkNotNull(dispute.getDepositTxSerialized(), "depositTxSerialized must not be null"); @@ -245,22 +247,7 @@ private static void testIfDisputeTriesReplay(Dispute disputeToTest, try { String disputeToTestTradeId = disputeToTest.getTradeId(); - // For pre v1.4.0 we do not get the delayed payout tx sent in mediation cases but in refund agent case we do. - // With 1.4.0 we send the delayed payout tx also in mediation cases. For v5 protocol trades, there is no DPT - // and it is unknown which staged txs will be published, if any, so they are only sent in refund agent cases. - if (disputeToTest.getSupportType() == SupportType.REFUND) { - if (disputeToTest.getWarningTxId() == null) { - checkNotNull(disputeToTest.getDelayedPayoutTxId(), - "Delayed payout transaction ID is null. " + - "Trade ID: %s", disputeToTestTradeId); - } else { - checkNotNull(disputeToTest.getRedirectTxId(), - "Redirect transaction ID is null. " + - "Trade ID: %s", disputeToTestTradeId); - } - } - checkNotNull(disputeToTest.getDepositTxId(), - "depositTxId must not be null. Trade ID: %s", disputeToTestTradeId); + checkTxIdFieldCombination(disputeToTest); checkNotNull(disputeToTest.getUid(), "agentsUid must not be null. Trade ID: %s", disputeToTestTradeId); @@ -280,6 +267,27 @@ private static void testIfDisputeTriesReplay(Dispute disputeToTest, } } + private static void checkTxIdFieldCombination(Dispute dispute) { + // For pre v1.4.0 we do not get the delayed payout tx sent in mediation cases but in refund agent case we do. + // With 1.4.0 we send the delayed payout tx also in mediation cases. For v5 protocol trades, there is no DPT + // and it is unknown which staged txs will be published, if any, so they are only sent in refund agent cases. + String tradeId = dispute.getTradeId(); + if (dispute.getWarningTxId() != null || dispute.getRedirectTxId() != null) { + checkNotNull(dispute.getWarningTxId(), "warningTxId must not be null. Trade ID: %s", tradeId); + checkNotNull(dispute.getRedirectTxId(), "redirectTxId must not be null. Trade ID: %s", tradeId); + checkArgument(dispute.getDelayedPayoutTxId() == null, + "delayedPayoutTxId should be null. Trade ID: %s", tradeId); + checkArgument(!dispute.isUsingLegacyBurningMan(), + "Legacy BM use not permitted. Trade ID: %s", tradeId); + checkArgument(dispute.getSupportType() == SupportType.REFUND, + "Mediation not permitted after staged txs published. Trade ID: %s", tradeId); + } else if (dispute.getSupportType() == SupportType.REFUND) { + checkNotNull(dispute.getDelayedPayoutTxId(), + "delayedPayoutTxId must not be null. Trade ID: %s", tradeId); + } + checkNotNull(dispute.getDepositTxId(), "depositTxId must not be null. Trade ID: %s", tradeId); + } + private enum DisputeIdField implements Function { TRADE_ID(Dispute::getTradeId), DELAYED_PAYOUT_TX_ID(Dispute::getDelayedPayoutTxId), 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 bd021606bc..890381a86f 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 @@ -41,6 +41,7 @@ import bisq.core.trade.TradeManager; import bisq.core.trade.bisq_v1.FailedTradesManager; import bisq.core.trade.model.bisq_v1.Trade; +import bisq.core.trade.protocol.bisq_v5.model.StagedPayoutTxParameters; import bisq.network.p2p.AckMessageSourceType; import bisq.network.p2p.NodeAddress; @@ -64,6 +65,7 @@ import com.google.inject.Singleton; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -262,10 +264,7 @@ public NodeAddress getAgentNodeAddress(Dispute dispute) { return dispute.getContract().getRefundAgentNodeAddress(); } - public CompletableFuture> requestBlockchainTransactions(String makerFeeTxId, - String takerFeeTxId, - String depositTxId, - String delayedPayoutTxId) { + public CompletableFuture> requestBlockchainTransactions(List txIds) { // 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()) { @@ -276,28 +275,28 @@ public CompletableFuture> requestBlockchainTransactions(String NetworkParameters params = btcWalletService.getParams(); List txs = new ArrayList<>(); - return mempoolService.requestTxAsHex(makerFeeTxId) - .thenCompose(txAsHex -> { - txs.add(new Transaction(params, Hex.decode(txAsHex))); - return mempoolService.requestTxAsHex(takerFeeTxId); - }).thenCompose(txAsHex -> { - txs.add(new Transaction(params, Hex.decode(txAsHex))); - return mempoolService.requestTxAsHex(depositTxId); - }).thenCompose(txAsHex -> { - txs.add(new Transaction(params, Hex.decode(txAsHex))); - return mempoolService.requestTxAsHex(delayedPayoutTxId); - }) - .thenApply(txAsHex -> { - txs.add(new Transaction(params, Hex.decode(txAsHex))); - return txs; - }); + Iterator txIdIterator = txIds.iterator(); + if (!txIdIterator.hasNext()) { + return CompletableFuture.completedFuture(txs); + } + CompletableFuture future = mempoolService.requestTxAsHex(txIdIterator.next()); + while (txIdIterator.hasNext()) { + String txId = txIdIterator.next(); + future = future.thenCompose(txAsHex -> { + txs.add(new Transaction(params, Hex.decode(txAsHex))); + return mempoolService.requestTxAsHex(txId); + }); + } + return future.thenApply(txAsHex -> { + txs.add(new Transaction(params, Hex.decode(txAsHex))); + return txs; + }); } public void verifyTradeTxChain(List txs) { Transaction makerFeeTx = txs.get(0); Transaction takerFeeTx = txs.get(1); Transaction depositTx = txs.get(2); - Transaction delayedPayoutTx = txs.get(3); // The order and number of buyer and seller inputs are not part of the trade protocol consensus. // In the current implementation buyer inputs come before seller inputs at depositTx and there is @@ -316,38 +315,85 @@ public void verifyTradeTxChain(List txs) { } checkArgument(makerFeeTxFoundAtInputs, "makerFeeTx not found at depositTx inputs"); checkArgument(takerFeeTxFoundAtInputs, "takerFeeTx not found at depositTx inputs"); - checkArgument(depositTx.getInputs().size() >= 2, - "DepositTx must have at least 2 inputs"); - checkArgument(delayedPayoutTx.getInputs().size() == 1, - "DelayedPayoutTx must have 1 input"); - TransactionOutPoint delayedPayoutTxInputOutpoint = delayedPayoutTx.getInputs().get(0).getOutpoint(); - String fundingTxId = delayedPayoutTxInputOutpoint.getHash().toString(); - checkArgument(fundingTxId.equals(depositTx.getTxId().toString()), - "First input at delayedPayoutTx does not connect to depositTx"); + checkArgument(depositTx.getInputs().size() >= 2, "depositTx must have at least 2 inputs"); + if (txs.size() == 4) { + Transaction delayedPayoutTx = txs.get(3); + checkArgument(delayedPayoutTx.getInputs().size() == 1, "delayedPayoutTx must have 1 input"); + checkArgument(firstOutputConnectsToFirstInput(depositTx, delayedPayoutTx), + "First input at delayedPayoutTx does not connect to depositTx"); + } else { + Transaction warningTx = txs.get(3); + Transaction redirectTx = txs.get(4); + + checkArgument(warningTx.getInputs().size() == 1, "warningTx must have 1 input"); + checkArgument(warningTx.getOutputs().size() == 2, "warningTx must have 2 outputs"); + checkArgument(warningTx.getOutput(1).getValue().value == + StagedPayoutTxParameters.WARNING_TX_FEE_BUMP_OUTPUT_VALUE, + "Second warningTx output is wrong amount for a fee bump output"); + + checkArgument(redirectTx.getInputs().size() == 1, "redirectTx must have 1 input"); + int numReceivers = redirectTx.getOutputs().size() - 1; + checkArgument(redirectTx.getOutput(numReceivers).getValue().value == + StagedPayoutTxParameters.REDIRECT_TX_FEE_BUMP_OUTPUT_VALUE, + "Last redirectTx output is wrong amount for a fee bump output"); + + checkArgument(firstOutputConnectsToFirstInput(depositTx, warningTx), + "First input at warningTx does not connect to depositTx"); + checkArgument(firstOutputConnectsToFirstInput(warningTx, redirectTx), + "First input at redirectTx does not connect to warningTx"); + } } - public void verifyDelayedPayoutTxReceivers(Transaction delayedPayoutTx, Dispute dispute) { - Transaction depositTx = dispute.findDepositTx(btcWalletService).orElseThrow(); + private static boolean firstOutputConnectsToFirstInput(Transaction parent, Transaction child) { + TransactionOutPoint childTxInputOutpoint = child.getInput(0).getOutpoint(); + String fundingTxId = childTxInputOutpoint.getHash().toString(); + return fundingTxId.equals(parent.getTxId().toString()); + } + + public void verifyDelayedPayoutTxReceivers(Transaction depositTx, Transaction delayedPayoutTx, Dispute dispute) { long inputAmount = depositTx.getOutput(0).getValue().value; int selectionHeight = dispute.getBurningManSelectionHeight(); - List> delayedPayoutTxReceivers = delayedPayoutTxReceiverService.getReceivers( + List> receivers = delayedPayoutTxReceiverService.getReceivers( selectionHeight, inputAmount, dispute.getTradeTxFee(), DelayedPayoutTxReceiverService.ReceiverFlag.flagsActivatedBy(dispute.getTradeDate())); - log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers); - checkArgument(delayedPayoutTx.getOutputs().size() == delayedPayoutTxReceivers.size(), - "Size of outputs and delayedPayoutTxReceivers must be the same"); + log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, receivers); + checkArgument(delayedPayoutTx.getOutputs().size() == receivers.size(), + "Number of outputs must equal number of receivers"); + checkOutputsPrefixMatchesReceivers(delayedPayoutTx, receivers); + } + + public void verifyRedirectTxReceivers(Transaction warningTx, Transaction redirectTx, Dispute dispute) { + long inputAmount = warningTx.getOutput(0).getValue().value; + long inputAmountMinusFeeBumpAmount = inputAmount - StagedPayoutTxParameters.REDIRECT_TX_FEE_BUMP_OUTPUT_VALUE; + int selectionHeight = dispute.getBurningManSelectionHeight(); + + List> receivers = delayedPayoutTxReceiverService.getReceivers( + selectionHeight, + inputAmountMinusFeeBumpAmount, + dispute.getTradeTxFee(), + StagedPayoutTxParameters.REDIRECT_TX_MIN_WEIGHT, + DelayedPayoutTxReceiverService.ReceiverFlag.flagsActivatedBy(dispute.getTradeDate())); + log.info("Verify redirectTx using selectionHeight {} and receivers {}", selectionHeight, receivers); + checkArgument(redirectTx.getOutputs().size() == receivers.size() + 1, + "Number of outputs must equal number of receivers plus 1"); + checkOutputsPrefixMatchesReceivers(redirectTx, receivers); + } + private void checkOutputsPrefixMatchesReceivers(Transaction delayedPayoutOrRedirectTx, + List> receivers) { NetworkParameters params = btcWalletService.getParams(); - for (int i = 0; i < delayedPayoutTx.getOutputs().size(); i++) { - TransactionOutput transactionOutput = delayedPayoutTx.getOutputs().get(i); - Tuple2 receiverTuple = delayedPayoutTxReceivers.get(0); + for (int i = 0; i < receivers.size(); i++) { + TransactionOutput transactionOutput = delayedPayoutOrRedirectTx.getOutput(i); + Tuple2 receiverTuple = receivers.get(i); checkArgument(transactionOutput.getScriptPubKey().getToAddress(params).toString().equals(receiverTuple.second), - "output address does not match delayedPayoutTxReceivers address. transactionOutput=" + transactionOutput); + "Output address does not match receiver address (%s). transactionOutput=%s", + receiverTuple.second, transactionOutput); checkArgument(transactionOutput.getValue().value == receiverTuple.first, - "output value does not match delayedPayoutTxReceivers value. transactionOutput=" + transactionOutput); + "Output value does not match receiver value (%s). transactionOutput=%s", + receiverTuple.first, transactionOutput); } } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 7bf0781003..7a1e8a03e8 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2997,9 +2997,9 @@ disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status disputeSummaryWindow.requestingTxs=Requesting blockchain transactions from block explorer... -disputeSummaryWindow.requestTransactionsError=Requesting the 4 trade transactions failed. Error message: {0}.\n\n\ +disputeSummaryWindow.requestTransactionsError=Requesting the {0} trade transactions failed. Error message: {1}.\n\n\ Please verify the transactions manually before closing the dispute. -disputeSummaryWindow.delayedPayoutTxVerificationFailed=Verification of the delayed payout transaction failed. Error message: {0}.\n\n\ +disputeSummaryWindow.delayedPayoutTxVerificationFailed=Verification of the delayed payout / redirection transaction failed. Error message: {0}.\n\n\ Please do not make the payout but get in touch with developers to clarify the case. # dynamic values are not recognized by IntelliJ 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 78c03b2048..42050d1c03 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 @@ -86,7 +86,9 @@ import java.time.Instant; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -123,7 +125,7 @@ public class DisputeSummaryWindow extends Overlay { // Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window. private Optional peersDisputeOptional; private String role; - private Label delayedPayoutTxStatus; + private Label delayedPayoutOrRedirectTxStatus; private TextArea summaryNotesTextArea; private ChangeListener customRadioButtonSelectedListener, buyerGetsTradeAmountSelectedListener, sellerGetsTradeAmountSelectedListener; @@ -168,8 +170,7 @@ public void show(Dispute dispute) { width = 1150; createGridPane(); addContent(); - // TODO: In case of v5 protocol, check confirmation of redirect tx instead. - checkDelayedPayoutTransaction(); + checkDelayedPayoutOrRedirectTransaction(); display(); if (DevEnv.isDevMode()) { @@ -227,7 +228,7 @@ private void addContent() { ? new DisputeResult(dispute.getTradeId(), dispute.getTraderId()) : dispute.getDisputeResultProperty().get(); - peersDisputeOptional = checkNotNull(getDisputeManager(dispute)).getDisputesAsObservableList().stream() + peersDisputeOptional = checkNotNull(getDisputeManager()).getDisputesAsObservableList().stream() .filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) .findFirst(); @@ -237,9 +238,9 @@ private void addContent() { addPayoutAmountTextFields(); addReasonControls(); applyDisputeResultToUiControls(); - boolean applyPeersDisputeResult = peersDisputeOptional.isPresent() && ( - peersDisputeOptional.get().getDisputeState() == Dispute.State.RESULT_PROPOSED || - peersDisputeOptional.get().getDisputeState() == Dispute.State.CLOSED); + boolean applyPeersDisputeResult = peersDisputeOptional.map(Dispute::getDisputeState) + .map(s -> s == Dispute.State.RESULT_PROPOSED || s == Dispute.State.CLOSED) + .orElse(false); if (applyPeersDisputeResult) { // If the other peers dispute has been closed we apply the result to ourselves DisputeResult peersDisputeResult = peersDisputeOptional.get().getDisputeResultProperty().get(); @@ -326,7 +327,8 @@ private void addInfoPane() { addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.extraInfo"), extraDataSummary.toString()); } } else { - delayedPayoutTxStatus = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.delayedPayoutStatus"), "Checking...").second; + delayedPayoutOrRedirectTxStatus = addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("disputeSummaryWindow.delayedPayoutStatus"), "Checking...").second; } } @@ -413,7 +415,7 @@ private boolean isPayoutAmountValid() { .add(offer.getSellerSecurityDeposit()); Coin totalAmount = buyerAmount.add(sellerAmount); - boolean isRefundAgent = getDisputeManager(dispute) instanceof RefundManager; + boolean isRefundAgent = getDisputeManager() instanceof RefundManager; if (isRefundAgent) { // We allow to spend less in case of RefundAgent or even zero to both, so in that case no payout tx will // be made @@ -710,7 +712,7 @@ protected void addButtons() { cancelButton.setOnAction(e -> { dispute.setDisputeResult(disputeResult); - checkNotNull(getDisputeManager(dispute)).requestPersistence(); + checkNotNull(getDisputeManager()).requestPersistence(); hide(); }); } @@ -840,28 +842,20 @@ public void onFailure(TxBroadcastException exception) { } } - // TODO: Need to adapt this to the v5 protocol case. Also, in that case, we should probably check that the tx fees - // paid by the warning & redirect txs don't reduce the amount going to the BM by too much, say by no more than the - // trade security deposit amount, as the fees could spike (or be manipulated) and in general the redirect tx pays - // out a final sum that will be less than the total trade collateral (due to fees). Finally, we should make sure - // that the refund agent can see from the UI whether the redirect tx has confirmed, before paying out. + // TODO: We should probably check that the tx fees paid by the warning & redirect txs (or DPT for v4) don't reduce + // the amount going to the BM by too much, say by no more than the trade security deposit amount, as the fees could + // spike (or be manipulated) and in general the redirect tx pays out a final sum that will be less than the total + // trade collateral (due to fees). private CompletableFuture maybeCheckTransactions() { final CompletableFuture asyncStatus = new CompletableFuture<>(); - var disputeManager = getDisputeManager(dispute); + var disputeManager = getDisputeManager(); // Only RefundAgent need to verify transactions to ensure payout is safe 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(); + List txIdChain = getTradeTxIdChain(); Popup requestingTxsPopup = new Popup().information(Res.get("disputeSummaryWindow.requestingTxs")).hideCloseButton(); requestingTxsPopup.show(); - refundManager.requestBlockchainTransactions(makerFeeTxId, - takerFeeTxId, - depositTxId, - delayedPayoutTxId + refundManager.requestBlockchainTransactions(txIdChain ).whenComplete((txList, throwable) -> UserThread.execute(() -> { requestingTxsPopup.hide(); @@ -869,28 +863,35 @@ private CompletableFuture maybeCheckTransactions() { try { refundManager.verifyTradeTxChain(txList); if (!dispute.isUsingLegacyBurningMan()) { - Transaction delayedPayoutTx = txList.get(3); - refundManager.verifyDelayedPayoutTxReceivers(delayedPayoutTx, dispute); + Transaction depositTx = txList.get(2); + if (txList.size() == 4) { + Transaction delayedPayoutTx = txList.get(3); + refundManager.verifyDelayedPayoutTxReceivers(depositTx, delayedPayoutTx, dispute); + } else { + Transaction warningTx = txList.get(3); + Transaction redirectTx = txList.get(4); + refundManager.verifyRedirectTxReceivers(warningTx, redirectTx, dispute); + } } asyncStatus.complete(true); } catch (Throwable error) { - 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(); - }, + UserThread.runAfter(() -> new 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(() -> asyncStatus.complete(true)) - .onClose(() -> asyncStatus.complete(false)) - .show(), + UserThread.runAfter(() -> new Popup() + .warning(Res.get("disputeSummaryWindow.requestTransactionsError", + txIdChain.size(), throwable.getMessage())) + .onAction(() -> asyncStatus.complete(true)) + .onClose(() -> asyncStatus.complete(false)) + .show(), 100, TimeUnit.MILLISECONDS); } @@ -901,14 +902,26 @@ private CompletableFuture maybeCheckTransactions() { return asyncStatus; } + private List getTradeTxIdChain() { + Contract contract = dispute.getContract(); + String makerFeeTxId = contract.getOfferPayload().getOfferFeePaymentTxId(); + String takerFeeTxId = contract.getTakerFeeTxID(); + String depositTxId = dispute.getDepositTxId(); + String warningTxId = dispute.getWarningTxId(); + return warningTxId != null + ? Arrays.asList(makerFeeTxId, takerFeeTxId, depositTxId, warningTxId, dispute.getRedirectTxId()) + : Arrays.asList(makerFeeTxId, takerFeeTxId, depositTxId, dispute.getDelayedPayoutTxId()); + } + private CompletableFuture checkGeneralValidity() { final CompletableFuture asyncStatus = new CompletableFuture<>(); - var disputeManager = checkNotNull(getDisputeManager(dispute)); + var disputeManager = checkNotNull(getDisputeManager()); try { DisputeValidation.testIfDisputeTriesReplay(dispute, disputeManager.getDisputesAsObservableList()); if (dispute.isUsingLegacyBurningMan()) { - DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade); + DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, + dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade); } asyncStatus.complete(true); } catch (DisputeValidation.AddressException exception) { @@ -961,7 +974,7 @@ private CompletableFuture checkGeneralValidity() { } private void applyDisputeResult(Button closeTicketButton) { - DisputeManager> disputeManager = getDisputeManager(dispute); + DisputeManager> disputeManager = getDisputeManager(); if (disputeManager == null) { return; } @@ -1028,7 +1041,7 @@ private void applyDisputeResult(Button closeTicketButton) { hide(); } - private DisputeManager> getDisputeManager(Dispute dispute) { + private DisputeManager> getDisputeManager() { if (dispute.getSupportType() != null) { switch (dispute.getSupportType()) { case ARBITRATION: @@ -1050,7 +1063,7 @@ private DisputeManager> getDisputeManager(Dispute /////////////////////////////////////////////////////////////////////////////////////////// private boolean isMediationDispute() { - return getDisputeManager(dispute) instanceof MediationManager; + return getDisputeManager() instanceof MediationManager; } // called when a radio button or amount box ui control is changed @@ -1181,24 +1194,30 @@ private void applyDisputeResultToUiControls() { updatingUi = false; } - private void checkDelayedPayoutTransaction() { - if (dispute.getDelayedPayoutTxId() == null) - return; - mempoolService.checkTxIsConfirmed(dispute.getDelayedPayoutTxId(), (validator -> { - long confirms = validator.parseJsonValidateTx(); - log.info("Mempool check confirmation status of DelayedPayoutTxId returned: [{}]", confirms); - displayPayoutStatus(confirms); - })); + private void checkDelayedPayoutOrRedirectTransaction() { + if (dispute.getRedirectTxId() != null) { + mempoolService.checkTxIsConfirmed(dispute.getRedirectTxId(), (validator -> { + long confirms = validator.parseJsonValidateTx(); + log.info("Mempool check confirmation status of redirectTxId returned: [{}]", confirms); + displayPayoutStatus(confirms); + })); + } else if (dispute.getDelayedPayoutTxId() != null) { + mempoolService.checkTxIsConfirmed(dispute.getDelayedPayoutTxId(), (validator -> { + long confirms = validator.parseJsonValidateTx(); + log.info("Mempool check confirmation status of delayedPayoutTxId returned: [{}]", confirms); + displayPayoutStatus(confirms); + })); + } } private void displayPayoutStatus(long nConfirmStatus) { - if (delayedPayoutTxStatus != null) { + if (delayedPayoutOrRedirectTxStatus != null) { String status = Res.get("confidence.unknown"); if (nConfirmStatus == 0) status = Res.get("confidence.seen", 1); else if (nConfirmStatus > 0) status = Res.get("confidence.confirmed", nConfirmStatus); - delayedPayoutTxStatus.setText(status); + delayedPayoutOrRedirectTxStatus.setText(status); } } }