Refactor closing ticket logic to fix #6517.

This commit is contained in:
jmacxx 2023-01-18 08:27:46 -06:00
parent 975210d228
commit 3dde7c6261
No known key found for this signature in database
GPG Key ID: 155297BABFE94A1B
2 changed files with 91 additions and 49 deletions

View File

@ -264,6 +264,14 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
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<List<Transaction>> retFuture = new CompletableFuture<>();
UserThread.runAfter(() -> retFuture.complete(new ArrayList<>()), 5);
return retFuture;
}
NetworkParameters params = btcWalletService.getParams();
List<Transaction> txs = new ArrayList<>();
return mempoolService.requestTxAsHex(makerFeeTxId)

View File

@ -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<DisputeSummaryWindow> {
setReasonRadioButtonState();
addSummaryNotes();
addButtons(contract);
addButtons();
}
private void addInfoPane() {
@ -674,7 +673,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
summaryNotesTextArea.textProperty().bindBidirectional(disputeResult.summaryNotesProperty());
}
private void addButtons(Contract contract) {
protected void addButtons() {
Tuple3<Button, Button, HBox> tuple = add2ButtonsWithBox(gridPane, ++rowIndex,
Res.get("disputeSummaryWindow.close.button"),
Res.get("shared.cancel"), 15, true);
@ -697,14 +696,22 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
return;
}
if (dispute.getSupportType() == SupportType.REFUND &&
peersDisputeOptional.isPresent() &&
!peersDisputeOptional.get().isClosed()) {
showPayoutTxConfirmation(contract,
disputeResult,
() -> doCloseAfterTxsVerified(closeTicketButton));
if (peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed()) {
closeTicket(closeTicketButton); // all checks done already on peers ticket
} else {
doCloseAfterTxsVerified(closeTicketButton);
maybeCheckTransactions().thenAccept(continue1 -> {
if (continue1) {
checkGeneralValidity().thenAccept(continue2 -> {
if (continue2) {
maybeMakePayout().thenAccept(continue3 -> {
if (continue3) {
closeTicket(closeTicketButton);
}
});
}
});
}
});
}
});
@ -715,21 +722,34 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
});
}
private void showPayoutTxConfirmation(Contract contract, DisputeResult disputeResult, ResultHandler resultHandler) {
private CompletableFuture<Boolean> maybeMakePayout() {
final CompletableFuture<Boolean> 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<Coin, Integer> feeTuple = txFeeEstimationService.getEstimatedFeeAndTxVsize(outputAmount, btcWalletService);
Coin fee = feeTuple.first;
@ -767,11 +787,14 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
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<DisputeSummaryWindow> {
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<DisputeSummaryWindow> {
Coin fee,
String buyerPayoutAddressString,
String sellerPayoutAddressString,
ResultHandler resultHandler) {
CompletableFuture<Boolean> 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<DisputeSummaryWindow> {
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<Boolean> maybeCheckTransactions() {
final CompletableFuture<Boolean> 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<DisputeSummaryWindow> {
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<DisputeSummaryWindow> {
});
});
} else {
doCloseIfValid(closeTicketButton);
asyncStatus.complete(true);
}
return asyncStatus;
}
private void doCloseIfValid(Button closeTicketButton) {
private CompletableFuture<Boolean> checkGeneralValidity() {
final CompletableFuture<Boolean> 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<DisputeSummaryWindow> {
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<DisputeSummaryWindow> {
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<DisputeSummaryWindow> {
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<? extends DisputeList<Dispute>> disputeManager = getDisputeManager(dispute);
if (disputeManager == null) {
return;