refactor arbitration protocol

add dispute states and open/close messages routed through arbitrator
both traders publish dispute payout tx, winner is default
verify signatures of payment sent and received messages
seller sends deposit confirmed message to arbitrator
buyer sends payment sent message to arbitrator
arbitrator slows trade wallet sync rate after deposits confirmed
various refactoring, fixes, and cleanup
This commit is contained in:
woodser 2022-11-04 15:56:53 -04:00
parent 363f783f30
commit 247087ef46
79 changed files with 1770 additions and 2480 deletions

View file

@ -33,7 +33,7 @@ import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
import bisq.core.trade.protocol.tasks.MakerSetLockTime;
import bisq.core.trade.protocol.tasks.RemoveOffer;
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
import bisq.core.trade.protocol.tasks.ProcessPaymentSentMessage;
import bisq.core.trade.protocol.tasks.SellerPublishDepositTx;
import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
@ -100,7 +100,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
SellerPublishDepositTx.class,
SellerPublishTradeStatistics.class,
SellerProcessPaymentSentMessage.class,
ProcessPaymentSentMessage.class,
ApplyFilter.class,
TakerVerifyMakerFeePayment.class,
@ -157,7 +157,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
SellerPublishDepositTx.class,
SellerPublishTradeStatistics.class,
SellerProcessPaymentSentMessage.class,
ProcessPaymentSentMessage.class,
ApplyFilter.class,
ApplyFilter.class,

View file

@ -92,10 +92,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private final CoinFormatter formatter;
private final ArbitrationManager arbitrationManager;
private final MediationManager mediationManager;
private final XmrWalletService walletService;
private final TradeWalletService tradeWalletService; // TODO (woodser): remove for xmr or adapt to get/create multisig wallets for tx creation utils
private final CoreDisputesService disputesService;
private Dispute dispute;
private final CoreDisputesService disputesService; private Dispute dispute;
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
private DisputeResult disputeResult;
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
@ -115,7 +112,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private ChangeListener<Toggle> reasonToggleSelectionListener;
private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField;
private ChangeListener<String> buyerPayoutAmountListener, sellerPayoutAmountListener;
private CheckBox isLoserPublisherCheckBox;
private ChangeListener<Toggle> tradeAmountToggleGroupListener;
@ -134,8 +130,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
this.formatter = formatter;
this.arbitrationManager = arbitrationManager;
this.mediationManager = mediationManager;
this.walletService = walletService;
this.tradeWalletService = tradeWalletService;
this.disputesService = disputesService;
type = Type.Confirmation;
@ -220,7 +214,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount());
disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount());
disputeResult.setWinner(peersDisputeResult.getWinner());
disputeResult.setLoserPublisher(peersDisputeResult.isLoserPublisher());
disputeResult.setReason(peersDisputeResult.getReason());
disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get());
@ -248,13 +241,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
reasonWasPeerWasLateRadioButton.setDisable(true);
reasonWasTradeAlreadySettledRadioButton.setDisable(true);
isLoserPublisherCheckBox.setDisable(true);
isLoserPublisherCheckBox.setSelected(peersDisputeResult.isLoserPublisher());
applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get());
applyTradeAmountRadioButtonStates();
} else {
isLoserPublisherCheckBox.setSelected(false);
}
setReasonRadioButtonState();
@ -426,11 +414,9 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller"));
sellerPayoutAmountInputTextField.setEditable(false);
isLoserPublisherCheckBox = new AutoTooltipCheckBox(Res.get("disputeSummaryWindow.payoutAmount.invert"));
VBox vBox = new VBox();
vBox.setSpacing(15);
vBox.getChildren().addAll(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField, isLoserPublisherCheckBox);
vBox.getChildren().addAll(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField);
GridPane.setMargin(vBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
GridPane.setRowIndex(vBox, rowIndex);
GridPane.setColumnIndex(vBox, 1);
@ -590,7 +576,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
Button cancelButton = tuple.second;
closeTicketButton.setOnAction(e -> {
disputesService.applyDisputePayout(dispute, disputeResult, contract);
doClose(closeTicketButton);
// if (dispute.getDepositTxSerialized() == null) {
@ -763,19 +748,14 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
boolean isRefundAgent = disputeManager instanceof RefundManager;
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
disputeResult.setCloseDate(new Date());
disputesService.closeDispute(disputeManager, dispute, disputeResult, isRefundAgent);
disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, () -> {
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
new Popup().attention(Res.get("disputeSummaryWindow.close.closePeer")).show();
}
disputeManager.requestPersistence();
});
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
UserThread.runAfter(() -> new Popup()
.attention(Res.get("disputeSummaryWindow.close.closePeer"))
.show(),
200, TimeUnit.MILLISECONDS);
}
disputeManager.requestPersistence();
closeTicketButton.disableProperty().unbind();
hide();
}

View file

@ -465,7 +465,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null;
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
String updatedMultisigHex = multisigWallet.exportMultisigHex();
if (trade.getPayoutTxId() != null) {
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
// payoutTxHashAsString = payoutTx.getHashAsString();
@ -477,9 +476,9 @@ public class PendingTradesDataModel extends ActivatableDataModel {
// If mediation is not activated we use arbitration
if (false) { // TODO (woodser): use mediation for xmr? if (MediationManager.isMediationActivated()) {
// In case we re-open a dispute we allow Trade.DisputeState.MEDIATION_REQUESTED or
useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED;
useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED || disputeState == Trade.DisputeState.DISPUTE_OPENED;
// in case of arbitration disputeState == Trade.DisputeState.ARBITRATION_REQUESTED
useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED;
useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED || disputeState == Trade.DisputeState.DISPUTE_OPENED;
} else {
useMediation = false;
useArbitration = true;
@ -549,27 +548,27 @@ public class PendingTradesDataModel extends ActivatableDataModel {
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex);
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
tradeManager.requestPersistence();
} else if (useArbitration) {
// Only if we have completed mediation we allow arbitration
disputeManager = arbitrationManager;
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex);
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
tradeManager.requestPersistence();
} else {
log.warn("Invalid dispute state {}", disputeState.name());
}
}
private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
disputeManager.sendOpenNewDisputeMessage(dispute, reOpen, senderMultisigHex,
private void sendDisputeOpenedMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
disputeManager.sendDisputeOpenedMessage(dispute, reOpen, senderMultisigHex,
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> {
if ((throwable instanceof DisputeAlreadyOpenException)) {
errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg");
new Popup().warning(errorMessage)
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
.onAction(() -> sendOpenNewDisputeMessage(dispute, true, disputeManager, senderMultisigHex))
.onAction(() -> sendDisputeOpenedMessage(dispute, true, disputeManager, senderMultisigHex))
.closeButtonText(Res.get("shared.cancel")).show();
} else {
new Popup().warning(errorMessage).show();

View file

@ -511,7 +511,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
if (trade instanceof ArbitratorTrade) return;
switch (payoutState) {
case PUBLISHED:
case PAYOUT_PUBLISHED:
sellerState.set(SellerState.STEP4);
buyerState.set(BuyerState.STEP4);
break;

View file

@ -31,6 +31,7 @@ import bisq.core.locale.Res;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.mediation.MediationResultState;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.Contract;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.TakerTrade;
@ -480,31 +481,25 @@ public abstract class TradeStepView extends AnchorPane {
switch (disputeState) {
case NO_DISPUTE:
break;
case DISPUTE_REQUESTED:
case DISPUTE_OPENED:
if (tradeStepInfo != null) {
tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText);
}
applyOnDisputeOpened();
// update trade view unless arbitrator
if (trade instanceof ArbitratorTrade) break;
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED);
});
break;
case DISPUTE_STARTED_BY_PEER:
if (tradeStepInfo != null) {
tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText);
}
applyOnDisputeOpened();
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
if (tradeStepInfo != null) {
boolean isOpener = dispute.isDisputeOpenerIsBuyer() ? trade.isBuyer() : trade.isSeller();
tradeStepInfo.setState(isOpener ? TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED : TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
}
});
break;
case DISPUTE_CLOSED:
break;
case MEDIATION_REQUESTED:

View file

@ -190,7 +190,7 @@ public class BuyerStep2View extends TradeStepView {
model.setMessageStateProperty(MessageState.FAILED);
break;
default:
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId());
log.warn("Unexpected case: State={}, tradeId={} ", state.name(), trade.getId());
busyAnimation.stop();
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
break;
@ -608,12 +608,6 @@ public class BuyerStep2View extends TradeStepView {
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
//TODO seems this was a hack to enable repeated confirm???
if (trade.isPaymentSent()) {
trade.setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
model.dataModel.getTradeManager().requestPersistence();
}
model.dataModel.onPaymentStarted(() -> {
}, errorMessage -> {
busyAnimation.stop();

View file

@ -145,6 +145,11 @@ public class SellerStep3View extends TradeStepView {
busyAnimation.stop();
statusLabel.setText("");
break;
case TRADE_COMPLETED:
if (!trade.isPayoutPublished()) log.warn("Payout is expected to be published for {} {} state {}", trade.getClass().getSimpleName(), trade.getId(), trade.getState());
busyAnimation.stop();
statusLabel.setText("");
break;
default:
log.warn("Unexpected case: State={}, tradeId={} " + state.name(), trade.getId());
busyAnimation.stop();