diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java index d046a16c982..3fbf0e58e98 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java @@ -67,6 +67,17 @@ public enum Reason { PEER_WAS_LATE } + public enum PayoutSuggestion { + UNKNOWN, + BUYER_GETS_TRADE_AMOUNT, + BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION, + BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY, + SELLER_GETS_TRADE_AMOUNT, + SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION, + SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY, + CUSTOM_PAYOUT + } + private final String tradeId; private final int traderId; @Setter @@ -91,6 +102,10 @@ public enum Reason { private long closeDate; @Setter private boolean isLoserPublisher; + @Setter + private String payoutAdjustmentPercent = ""; + @Setter + private PayoutSuggestion payoutSuggestion; public DisputeResult(String tradeId, int traderId) { this.tradeId = tradeId; @@ -111,7 +126,9 @@ public DisputeResult(String tradeId, long sellerPayoutAmount, @Nullable byte[] arbitratorPubKey, long closeDate, - boolean isLoserPublisher) { + boolean isLoserPublisher, + String payoutAdjustmentPercent, + PayoutSuggestion payoutSuggestion) { this.tradeId = tradeId; this.traderId = traderId; this.winner = winner; @@ -127,6 +144,8 @@ public DisputeResult(String tradeId, this.arbitratorPubKey = arbitratorPubKey; this.closeDate = closeDate; this.isLoserPublisher = isLoserPublisher; + this.payoutAdjustmentPercent = payoutAdjustmentPercent; + this.payoutSuggestion = payoutSuggestion; } @@ -149,7 +168,9 @@ public static DisputeResult fromProto(protobuf.DisputeResult proto) { proto.getSellerPayoutAmount(), proto.getArbitratorPubKey().toByteArray(), proto.getCloseDate(), - proto.getIsLoserPublisher()); + proto.getIsLoserPublisher(), + proto.getPayoutAdjustmentPercent(), + ProtoUtil.enumFromProto(DisputeResult.PayoutSuggestion.class, proto.getPayoutSuggestion().name())); } @Override @@ -165,13 +186,15 @@ public protobuf.DisputeResult toProtoMessage() { .setBuyerPayoutAmount(buyerPayoutAmount) .setSellerPayoutAmount(sellerPayoutAmount) .setCloseDate(closeDate) - .setIsLoserPublisher(isLoserPublisher); + .setIsLoserPublisher(isLoserPublisher) + .setPayoutAdjustmentPercent(payoutAdjustmentPercent); Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature))); Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey))); Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name()))); Optional.ofNullable(chatMessage).ifPresent(chatMessage -> builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())); + Optional.ofNullable(payoutSuggestion).ifPresent(result -> builder.setPayoutSuggestion(protobuf.DisputeResult.PayoutSuggestion.valueOf(payoutSuggestion.name()))); return builder.build(); } @@ -254,6 +277,8 @@ public String toString() { ",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) + ",\n closeDate=" + closeDate + ",\n isLoserPublisher=" + isLoserPublisher + + ",\n payoutAdjustmentPercent=" + payoutAdjustmentPercent + + ",\n payoutSuggestion=" + payoutSuggestion + "\n}"; } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 85f0c9c9262..42e0ddf1100 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2656,6 +2656,8 @@ disputeSummaryWindow.title=Summary disputeSummaryWindow.openDate=Ticket opening date disputeSummaryWindow.role=Trader's role disputeSummaryWindow.payout=Trade amount payout +disputeSummaryWindow.payout.getsCompensation=BTC {0} gets trade amount plus compensation +disputeSummaryWindow.payout.getsPenalty=BTC {0} gets trade amount minus penalty disputeSummaryWindow.payout.getsTradeAmount=BTC {0} gets trade amount payout disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0} disputeSummaryWindow.payout.custom=Custom payout diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index a3df97d74c1..c75f093f2e0 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -66,6 +66,7 @@ import org.junit.Test; import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById; +import static bisq.core.support.dispute.DisputeResult.PayoutSuggestion.UNKNOWN; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -213,7 +214,7 @@ public void testArbitratorSignWitness() { 0, null, now - 1, - false)); + false, "", UNKNOWN)); // Filtermanager says nothing is filtered when(filterManager.isNodeAddressBanned(any())).thenReturn(false); 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 ffbba10522e..82d8c9e7591 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 @@ -113,7 +113,8 @@ public class DisputeSummaryWindow extends Overlay { private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; private DisputeResult disputeResult; private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton, - buyerGetsAllRadioButton, sellerGetsAllRadioButton, customRadioButton; + buyerGetsCompensationRadioButton, sellerGetsCompensationRadioButton, + buyerGetsTradeAmountMinusPenaltyRadioButton, sellerGetsTradeAmountMinusPenaltyRadioButton, customRadioButton; private RadioButton reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, reasonProtocolViolationRadioButton, reasonNoReplyRadioButton, reasonWasScamRadioButton, reasonWasOtherRadioButton, reasonWasBankRadioButton, reasonWasOptionTradeRadioButton, @@ -126,13 +127,13 @@ public class DisputeSummaryWindow extends Overlay { private Label delayedPayoutTxStatus; private TextArea summaryNotesTextArea; - private ChangeListener customRadioButtonSelectedListener; + private ChangeListener customRadioButtonSelectedListener, buyerGetsTradeAmountSelectedListener, sellerGetsTradeAmountSelectedListener; private ChangeListener reasonToggleSelectionListener; - private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField; + private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField, compensationOrPenalty; private ChangeListener buyerPayoutAmountListener, sellerPayoutAmountListener; - private CheckBox isLoserPublisherCheckBox; private ChangeListener tradeAmountToggleGroupListener; - + private ChangeListener compensationOrPenaltyListener; + private boolean updatingUi = false; /////////////////////////////////////////////////////////////////////////////////////////// // Public API @@ -189,6 +190,12 @@ protected void cleanup() { if (customRadioButton != null) customRadioButton.selectedProperty().removeListener(customRadioButtonSelectedListener); + if (buyerGetsTradeAmountRadioButton != null) + buyerGetsTradeAmountRadioButton.selectedProperty().removeListener(buyerGetsTradeAmountSelectedListener); + + if (sellerGetsTradeAmountRadioButton != null) + sellerGetsTradeAmountRadioButton.selectedProperty().removeListener(sellerGetsTradeAmountSelectedListener); + if (tradeAmountToggleGroup != null) tradeAmountToggleGroup.selectedToggleProperty().removeListener(tradeAmountToggleGroupListener); @@ -210,7 +217,7 @@ protected void setupKeyHandler(Scene scene) { @Override protected void createGridPane() { super.createGridPane(); - gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.setPadding(new Insets(35, 40, 0, 40)); gridPane.getStyleClass().add("grid-pane"); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); gridPane.setPrefWidth(width); @@ -232,6 +239,7 @@ private void addContent() { addTradeAmountPayoutControls(); addPayoutAmountTextFields(); addReasonControls(); + applyDisputeResultToUiControls(); boolean applyPeersDisputeResult = peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed(); if (applyPeersDisputeResult) { @@ -239,21 +247,26 @@ private void addContent() { DisputeResult peersDisputeResult = peersDisputeOptional.get().getDisputeResultProperty().get(); disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount()); disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount()); + disputeResult.setPayoutAdjustmentPercent(peersDisputeResult.getPayoutAdjustmentPercent()); + disputeResult.setPayoutSuggestion(peersDisputeResult.getPayoutSuggestion()); disputeResult.setWinner(peersDisputeResult.getWinner()); - disputeResult.setLoserPublisher(peersDisputeResult.isLoserPublisher()); disputeResult.setReason(peersDisputeResult.getReason()); disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); buyerGetsTradeAmountRadioButton.setDisable(true); - buyerGetsAllRadioButton.setDisable(true); + buyerGetsCompensationRadioButton.setDisable(true); + buyerGetsTradeAmountMinusPenaltyRadioButton.setDisable(true); sellerGetsTradeAmountRadioButton.setDisable(true); - sellerGetsAllRadioButton.setDisable(true); + sellerGetsCompensationRadioButton.setDisable(true); + sellerGetsTradeAmountMinusPenaltyRadioButton.setDisable(true); customRadioButton.setDisable(true); buyerPayoutAmountInputTextField.setDisable(true); sellerPayoutAmountInputTextField.setDisable(true); + compensationOrPenalty.setDisable(true); buyerPayoutAmountInputTextField.setEditable(false); sellerPayoutAmountInputTextField.setEditable(false); + compensationOrPenalty.setEditable(false); reasonWasBugRadioButton.setDisable(true); reasonWasUsabilityIssueRadioButton.setDisable(true); @@ -267,14 +280,7 @@ private void addContent() { reasonWasWrongSenderAccountRadioButton.setDisable(true); reasonWasPeerWasLateRadioButton.setDisable(true); reasonWasTradeAlreadySettledRadioButton.setDisable(true); - - isLoserPublisherCheckBox.setDisable(true); - isLoserPublisherCheckBox.setSelected(peersDisputeResult.isLoserPublisher()); - - applyPayoutAmounts(tradeAmountToggleGroup.selectedToggleProperty().get()); - applyTradeAmountRadioButtonStates(); - } else { - isLoserPublisherCheckBox.setSelected(false); + applyDisputeResultToUiControls(); } setReasonRadioButtonState(); @@ -328,41 +334,51 @@ private void addInfoPane() { } private void addTradeAmountPayoutControls() { - buyerGetsTradeAmountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsTradeAmount", - Res.get("shared.buyer"))); - buyerGetsAllRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsAll", - Res.get("shared.buyer"))); - sellerGetsTradeAmountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsTradeAmount", - Res.get("shared.seller"))); - sellerGetsAllRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsAll", - Res.get("shared.seller"))); + buyerGetsTradeAmountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsTradeAmount", Res.get("shared.buyer"))); + buyerGetsCompensationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsCompensation", Res.get("shared.buyer"))); + buyerGetsTradeAmountMinusPenaltyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsPenalty", Res.get("shared.buyer"))); + sellerGetsTradeAmountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsTradeAmount", Res.get("shared.seller"))); + sellerGetsCompensationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsCompensation", Res.get("shared.seller"))); + sellerGetsTradeAmountMinusPenaltyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsPenalty", Res.get("shared.seller"))); customRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.custom")); VBox radioButtonPane = new VBox(); radioButtonPane.setSpacing(10); - radioButtonPane.getChildren().addAll(buyerGetsTradeAmountRadioButton, buyerGetsAllRadioButton, - sellerGetsTradeAmountRadioButton, sellerGetsAllRadioButton, + radioButtonPane.getChildren().addAll(buyerGetsTradeAmountRadioButton, buyerGetsCompensationRadioButton, + buyerGetsTradeAmountMinusPenaltyRadioButton, sellerGetsTradeAmountRadioButton, sellerGetsCompensationRadioButton, sellerGetsTradeAmountMinusPenaltyRadioButton, customRadioButton); addTopLabelWithVBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.payout"), radioButtonPane, 0); tradeAmountToggleGroup = new ToggleGroup(); buyerGetsTradeAmountRadioButton.setToggleGroup(tradeAmountToggleGroup); - buyerGetsAllRadioButton.setToggleGroup(tradeAmountToggleGroup); + buyerGetsCompensationRadioButton.setToggleGroup(tradeAmountToggleGroup); + buyerGetsTradeAmountMinusPenaltyRadioButton.setToggleGroup(tradeAmountToggleGroup); sellerGetsTradeAmountRadioButton.setToggleGroup(tradeAmountToggleGroup); - sellerGetsAllRadioButton.setToggleGroup(tradeAmountToggleGroup); + sellerGetsCompensationRadioButton.setToggleGroup(tradeAmountToggleGroup); + sellerGetsTradeAmountMinusPenaltyRadioButton.setToggleGroup(tradeAmountToggleGroup); customRadioButton.setToggleGroup(tradeAmountToggleGroup); - tradeAmountToggleGroupListener = (observable, oldValue, newValue) -> applyPayoutAmounts(newValue); + tradeAmountToggleGroupListener = (observable, oldValue, newValue) -> applyUpdateFromUi(newValue); tradeAmountToggleGroup.selectedToggleProperty().addListener(tradeAmountToggleGroupListener); buyerPayoutAmountListener = (observable, oldValue, newValue) -> applyCustomAmounts(buyerPayoutAmountInputTextField, oldValue, newValue); sellerPayoutAmountListener = (observable, oldValue, newValue) -> applyCustomAmounts(sellerPayoutAmountInputTextField, oldValue, newValue); + buyerGetsTradeAmountSelectedListener = (observable, oldValue, newValue) -> { + compensationOrPenalty.setEditable(!newValue); + }; + buyerGetsTradeAmountRadioButton.selectedProperty().addListener(buyerGetsTradeAmountSelectedListener); + + sellerGetsTradeAmountSelectedListener = (observable, oldValue, newValue) -> { + compensationOrPenalty.setEditable(!newValue); + }; + sellerGetsTradeAmountRadioButton.selectedProperty().addListener(sellerGetsTradeAmountSelectedListener); + customRadioButtonSelectedListener = (observable, oldValue, newValue) -> { buyerPayoutAmountInputTextField.setEditable(newValue); sellerPayoutAmountInputTextField.setEditable(newValue); - + compensationOrPenalty.setEditable(!newValue); if (newValue) { buyerPayoutAmountInputTextField.focusedProperty().addListener(buyerPayoutAmountListener); sellerPayoutAmountInputTextField.focusedProperty().addListener(sellerPayoutAmountListener); @@ -379,7 +395,6 @@ private void removePayoutAmountListeners() { if (sellerPayoutAmountInputTextField != null && sellerPayoutAmountListener != null) sellerPayoutAmountInputTextField.focusedProperty().removeListener(sellerPayoutAmountListener); - } private boolean isPayoutAmountValid() { @@ -486,15 +501,26 @@ private void addPayoutAmountTextFields() { sellerPayoutAmountInputTextField.setPromptText(Res.get("disputeSummaryWindow.payoutAmount.seller")); sellerPayoutAmountInputTextField.setEditable(false); - isLoserPublisherCheckBox = new AutoTooltipCheckBox(Res.get("disputeSummaryWindow.payoutAmount.invert")); + compensationOrPenalty = new InputTextField(); + compensationOrPenalty.setPromptText("Comp|Penalty percent"); + compensationOrPenalty.setLabelFloat(true); + HBox hBoxPenalty = new HBox(compensationOrPenalty); + HBox hBoxPayouts = new HBox(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField); + hBoxPayouts.setSpacing(15); VBox vBox = new VBox(); - vBox.setSpacing(15); - vBox.getChildren().addAll(buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField, isLoserPublisherCheckBox); - GridPane.setMargin(vBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + vBox.setSpacing(25); + vBox.getChildren().addAll(hBoxPenalty, hBoxPayouts); + GridPane.setMargin(vBox, new Insets(80, 50, 50, 50)); GridPane.setRowIndex(vBox, rowIndex); GridPane.setColumnIndex(vBox, 1); gridPane.getChildren().add(vBox); + + compensationOrPenaltyListener = (observable, oldValue, newValue) -> { + applyUpdateFromUi(tradeAmountToggleGroup.selectedToggleProperty().get()); + }; + + compensationOrPenalty.textProperty().addListener(compensationOrPenaltyListener); } private void addReasonControls() { @@ -628,7 +654,7 @@ private void addSummaryNotes() { Res.get("disputeSummaryWindow.summaryNotes"), summaryNotesTextArea, 0); GridPane.setColumnSpan(topLabelWithVBox.second, 2); - summaryNotesTextArea.setPrefHeight(50); + summaryNotesTextArea.setPrefHeight(160); summaryNotesTextArea.textProperty().bindBidirectional(disputeResult.summaryNotesProperty()); } @@ -833,7 +859,7 @@ private void doClose(Button closeTicketButton) { } boolean isRefundAgent = disputeManager instanceof RefundManager; - disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); + disputeResult.setLoserPublisher(false); // field no longer used per pazza / leo816 disputeResult.setCloseDate(new Date()); dispute.setDisputeResult(disputeResult); dispute.setIsClosed(); @@ -911,88 +937,136 @@ private DisputeManager> getDisputeManager(Dispute // Controller /////////////////////////////////////////////////////////////////////////////////////////// - private void applyPayoutAmounts(Toggle selectedTradeAmountToggle) { - if (selectedTradeAmountToggle != customRadioButton && selectedTradeAmountToggle != null) { - applyPayoutAmountsToDisputeResult(selectedTradeAmountToggle); - applyTradeAmountRadioButtonStates(); + private boolean isMediationDispute() { + return getDisputeManager(dispute) instanceof MediationManager; + } + + // called when a radio button or amount box ui control is changed + private void applyUpdateFromUi(Toggle selectedTradeAmountToggle) { + if (updatingUi || selectedTradeAmountToggle == null) { + return; } + applyUiControlsToDisputeResult(selectedTradeAmountToggle); + applyDisputeResultToUiControls(); } - private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) { + private void applyUiControlsToDisputeResult(Toggle selectedTradeAmountToggle) { Contract contract = dispute.getContract(); Offer offer = new Offer(contract.getOfferPayload()); Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit(); Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit(); Coin tradeAmount = contract.getTradeAmount(); - - boolean isMediationDispute = getDisputeManager(dispute) instanceof MediationManager; + Coin totalPot = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit); // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the // mediated payout. For Refund agent cases we do not have that restriction. - Coin minRefundAtDispute = isMediationDispute ? Restrictions.getMinRefundAtMediatedDispute() : Coin.ZERO; - Coin maxPayoutAmount = tradeAmount - .add(buyerSecurityDeposit) - .add(sellerSecurityDeposit) - .subtract(minRefundAtDispute); + Coin minRefundAtDispute = isMediationDispute() ? Restrictions.getMinRefundAtMediatedDispute() : Coin.ZERO; + + Coin penalizedPortionOfTradeAmount = Coin.ZERO; + try { + disputeResult.setPayoutAdjustmentPercent(compensationOrPenalty.getText().replaceAll("[^0-9]", "")); + double percentPenalty = ParsingUtils.parsePercentStringToDouble(disputeResult.getPayoutAdjustmentPercent()); + penalizedPortionOfTradeAmount = Coin.valueOf((long) (contract.getTradeAmount().getValue() * percentPenalty)); + } catch (NumberFormatException | NullPointerException e) { + log.warn(e.toString()); + } if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) { + disputeResult.setPayoutSuggestion(DisputeResult.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT); disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit)); disputeResult.setSellerPayoutAmount(sellerSecurityDeposit); - disputeResult.setWinner(DisputeResult.Winner.BUYER); - } else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) { - disputeResult.setBuyerPayoutAmount(maxPayoutAmount); - disputeResult.setSellerPayoutAmount(minRefundAtDispute); - disputeResult.setWinner(DisputeResult.Winner.BUYER); + disputeResult.setPayoutAdjustmentPercent(""); } else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) { + disputeResult.setPayoutSuggestion(DisputeResult.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT); disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit); disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit)); - disputeResult.setWinner(DisputeResult.Winner.SELLER); - } else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) { + disputeResult.setPayoutAdjustmentPercent(""); + } else if (selectedTradeAmountToggle == buyerGetsTradeAmountMinusPenaltyRadioButton) { + disputeResult.setPayoutSuggestion(DisputeResult.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY); + Coin buyerPayout = tradeAmount.add(offer.getBuyerSecurityDeposit()).subtract(penalizedPortionOfTradeAmount); + disputeResult.setBuyerPayoutAmount(buyerPayout); + disputeResult.setSellerPayoutAmount(totalPot.subtract(buyerPayout)); + } else if (selectedTradeAmountToggle == sellerGetsTradeAmountMinusPenaltyRadioButton) { + disputeResult.setPayoutSuggestion(DisputeResult.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY); + Coin sellerPayout = tradeAmount.add(offer.getBuyerSecurityDeposit()).subtract(penalizedPortionOfTradeAmount); + disputeResult.setSellerPayoutAmount(sellerPayout); + disputeResult.setBuyerPayoutAmount(totalPot.subtract(sellerPayout)); + } else if (selectedTradeAmountToggle == buyerGetsCompensationRadioButton) { + disputeResult.setPayoutSuggestion(DisputeResult.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION); + Coin buyerPayout = tradeAmount.add(offer.getBuyerSecurityDeposit()).add(penalizedPortionOfTradeAmount); + disputeResult.setBuyerPayoutAmount(buyerPayout); + disputeResult.setSellerPayoutAmount(totalPot.subtract(buyerPayout)); + } else if (selectedTradeAmountToggle == sellerGetsCompensationRadioButton) { + disputeResult.setPayoutSuggestion(DisputeResult.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION); + Coin sellerPayout = tradeAmount.add(offer.getSellerSecurityDeposit()).add(penalizedPortionOfTradeAmount); + disputeResult.setSellerPayoutAmount(sellerPayout); + disputeResult.setBuyerPayoutAmount(totalPot.subtract(sellerPayout)); + } else { + disputeResult.setPayoutSuggestion(DisputeResult.PayoutSuggestion.CUSTOM_PAYOUT); + disputeResult.setPayoutAdjustmentPercent(""); + } + + // enforce rule that we cannot pay out less than minRefundAtDispute + if (disputeResult.getBuyerPayoutAmount().isLessThan(minRefundAtDispute)) { disputeResult.setBuyerPayoutAmount(minRefundAtDispute); - disputeResult.setSellerPayoutAmount(maxPayoutAmount); - disputeResult.setWinner(DisputeResult.Winner.SELLER); + disputeResult.setSellerPayoutAmount(totalPot.subtract(minRefundAtDispute)); + } else if (disputeResult.getSellerPayoutAmount().isLessThan(minRefundAtDispute)) { + disputeResult.setSellerPayoutAmount(minRefundAtDispute); + disputeResult.setBuyerPayoutAmount(totalPot.subtract(minRefundAtDispute)); } - buyerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getBuyerPayoutAmount())); - sellerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getSellerPayoutAmount())); + // winner is the one who receives most from the multisig, or if equal, the buyer. + // (winner is used to decide who publishes the tx) + disputeResult.setWinner(disputeResult.getSellerPayoutAmount().isLessThan(disputeResult.getBuyerPayoutAmount()) ? + DisputeResult.Winner.BUYER : DisputeResult.Winner.BUYER); } - private void applyTradeAmountRadioButtonStates() { - Contract contract = dispute.getContract(); - Offer offer = new Offer(contract.getOfferPayload()); - Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit(); - Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit(); - Coin tradeAmount = contract.getTradeAmount(); - - Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); - Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); - - buyerPayoutAmountInputTextField.setText(formatter.formatCoin(buyerPayoutAmount)); - sellerPayoutAmountInputTextField.setText(formatter.formatCoin(sellerPayoutAmount)); - - boolean isMediationDispute = getDisputeManager(dispute) instanceof MediationManager; - // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the - // mediated payout. For Refund agent cases we do not have that restriction. - Coin minRefundAtDispute = isMediationDispute ? Restrictions.getMinRefundAtMediatedDispute() : Coin.ZERO; - Coin maxPayoutAmount = tradeAmount - .add(buyerSecurityDeposit) - .add(sellerSecurityDeposit) - .subtract(minRefundAtDispute); - - if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit)) && - sellerPayoutAmount.equals(sellerSecurityDeposit)) { + private void applyDisputeResultToUiControls() { + updatingUi = true; + buyerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getBuyerPayoutAmount())); + sellerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getSellerPayoutAmount())); + compensationOrPenalty.setText(disputeResult.getPayoutAdjustmentPercent()); + if (disputeResult.getPayoutSuggestion() == DisputeResult.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) { buyerGetsTradeAmountRadioButton.setSelected(true); - } else if (buyerPayoutAmount.equals(maxPayoutAmount) && - sellerPayoutAmount.equals(minRefundAtDispute)) { - buyerGetsAllRadioButton.setSelected(true); - } else if (sellerPayoutAmount.equals(tradeAmount.add(sellerSecurityDeposit)) - && buyerPayoutAmount.equals(buyerSecurityDeposit)) { + } else if (disputeResult.getPayoutSuggestion() == DisputeResult.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) { sellerGetsTradeAmountRadioButton.setSelected(true); - } else if (sellerPayoutAmount.equals(maxPayoutAmount) - && buyerPayoutAmount.equals(minRefundAtDispute)) { - sellerGetsAllRadioButton.setSelected(true); - } else { + } else if (disputeResult.getPayoutSuggestion() == DisputeResult.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION) { + buyerGetsCompensationRadioButton.setSelected(true); + } else if (disputeResult.getPayoutSuggestion() == DisputeResult.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION) { + sellerGetsCompensationRadioButton.setSelected(true); + } else if (disputeResult.getPayoutSuggestion() == DisputeResult.PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY) { + buyerGetsTradeAmountMinusPenaltyRadioButton.setSelected(true); + } else if (disputeResult.getPayoutSuggestion() == DisputeResult.PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY) { + sellerGetsTradeAmountMinusPenaltyRadioButton.setSelected(true); + } else if (disputeResult.getPayoutSuggestion() == DisputeResult.PayoutSuggestion.CUSTOM_PAYOUT) { customRadioButton.setSelected(true); + } else { + // the option was not set, this will apply to older records before PayoutSuggestion was persisted + // what it used to do was infer the option based on the payout amounts + Contract contract = dispute.getContract(); + Offer offer = new Offer(contract.getOfferPayload()); + Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit(); + Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit(); + Coin tradeAmount = contract.getTradeAmount(); + Coin totalPot = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit); + Coin minRefundAtDispute = isMediationDispute() ? Restrictions.getMinRefundAtMediatedDispute() : Coin.ZERO; + Coin maxPayoutAmount = totalPot.subtract(minRefundAtDispute); + if (disputeResult.getBuyerPayoutAmount().equals(tradeAmount.add(buyerSecurityDeposit)) && + disputeResult.getSellerPayoutAmount().equals(sellerSecurityDeposit)) { + buyerGetsTradeAmountRadioButton.setSelected(true); + } else if (disputeResult.getBuyerPayoutAmount().equals(maxPayoutAmount) && + disputeResult.getSellerPayoutAmount().equals(minRefundAtDispute)) { + buyerGetsCompensationRadioButton.setSelected(true); + } else if (disputeResult.getSellerPayoutAmount().equals(tradeAmount.add(sellerSecurityDeposit)) + && disputeResult.getBuyerPayoutAmount().equals(buyerSecurityDeposit)) { + sellerGetsTradeAmountRadioButton.setSelected(true); + } else if (disputeResult.getSellerPayoutAmount().equals(maxPayoutAmount) + && disputeResult.getBuyerPayoutAmount().equals(minRefundAtDispute)) { + sellerGetsCompensationRadioButton.setSelected(true); + } else { + customRadioButton.setSelected(true); + } } + updatingUi = false; } private void checkDelayedPayoutTransaction() { @@ -1016,3 +1090,4 @@ else if (nConfirmStatus > 0) } } } + diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index e305174798c..f7b8cd081b8 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -975,6 +975,16 @@ message DisputeResult { PEER_WAS_LATE = 12; } + enum PayoutSuggestion { + CUSTOM_PAYOUT = 0; + BUYER_GETS_TRADE_AMOUNT = 1; + BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION = 2; + BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY = 3; + SELLER_GETS_TRADE_AMOUNT = 4; + SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION = 5; + SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY = 6; + } + string trade_id = 1; int32 trader_id = 2; Winner winner = 3; @@ -990,6 +1000,8 @@ message DisputeResult { bytes arbitrator_pub_key = 13; int64 close_date = 14; bool is_loser_publisher = 15; + string payout_adjustment_percent = 16; + PayoutSuggestion payout_suggestion = 17; } ///////////////////////////////////////////////////////////////////////////////////////////