Add API functions to open and resolve disputes (#244)

Co-authored-by: woodser <woodser@protonmail.com>
This commit is contained in:
duriancrepe 2022-03-07 09:56:39 -08:00 committed by GitHub
parent 07c48a04f5
commit e7b4627102
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 752 additions and 306 deletions

View file

@ -228,8 +228,8 @@ public class NotificationCenter {
private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) {
String message = null;
if (refundManager.findOwnDispute(trade.getId()).isPresent()) {
String disputeOrTicket = refundManager.findOwnDispute(trade.getId()).get().isSupportTicket() ?
if (refundManager.findDispute(trade.getId()).isPresent()) {
String disputeOrTicket = refundManager.findDispute(trade.getId()).get().isSupportTicket() ?
Res.get("shared.supportTicket") :
Res.get("shared.dispute");
switch (disputeState) {
@ -253,8 +253,8 @@ public class NotificationCenter {
if (message != null) {
goToSupport(trade, message, false);
}
} else if (mediationManager.findOwnDispute(trade.getId()).isPresent()) {
String disputeOrTicket = mediationManager.findOwnDispute(trade.getId()).get().isSupportTicket() ?
} else if (mediationManager.findDispute(trade.getId()).isPresent()) {
String disputeOrTicket = mediationManager.findDispute(trade.getId()).get().isSupportTicket() ?
Res.get("shared.supportTicket") :
Res.get("shared.mediationCase");
switch (disputeState) {

View file

@ -23,7 +23,8 @@ import bisq.desktop.components.HavenoTextArea;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.support.dispute.DisputeSummaryVerification;
import bisq.core.api.CoreDisputesService;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.Layout;
@ -49,7 +50,6 @@ import bisq.common.handlers.ResultHandler;
import bisq.common.util.Tuple2;
import bisq.common.util.Tuple3;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import com.google.inject.Inject;
@ -86,11 +86,6 @@ import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private final CoinFormatter formatter;
@ -98,8 +93,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
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 Optional<Runnable> finalizeDisputeHandlerOptional = Optional.empty();
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
private DisputeResult disputeResult;
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
@ -132,13 +127,15 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
ArbitrationManager arbitrationManager,
MediationManager mediationManager,
XmrWalletService walletService,
TradeWalletService tradeWalletService) {
TradeWalletService tradeWalletService,
CoreDisputesService disputesService) {
this.formatter = formatter;
this.arbitrationManager = arbitrationManager;
this.mediationManager = mediationManager;
this.walletService = walletService;
this.tradeWalletService = tradeWalletService;
this.disputesService = disputesService;
type = Type.Confirmation;
}
@ -159,12 +156,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
}
}
public DisputeSummaryWindow onFinalizeDispute(Runnable finalizeDisputeHandler) {
this.finalizeDisputeHandlerOptional = Optional.of(finalizeDisputeHandler);
return this;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected
///////////////////////////////////////////////////////////////////////////////////////////
@ -598,39 +589,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
Button cancelButton = tuple.second;
closeTicketButton.setOnAction(e -> {
// TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master)
if (!dispute.isMediationDispute()) {
try {
System.out.println(disputeResult);
MoneroWallet multisigWallet = walletService.getMultisigWallet(dispute.getTradeId());
//dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract?
//disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey());
// TODO (woodser): don't send signed tx if opener is not co-signer?
// // determine if opener is co-signer
// boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER);
// boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher();
// if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx");
// arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex
boolean isOpener = dispute.isOpener();
System.out.println("Is dispute opener: " + isOpener);
if (isOpener) {
MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx);
if (arbitratorPayoutTx != null) disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex());
}
// send arbitrator's updated multisig hex with dispute result
disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.getMultisigHex());
} catch (AddressFormatException e2) {
log.error("Error at close dispute", e2);
return;
}
}
// // TODO (woodser): handle with showPayoutTxConfirmation() / doCloseIfValid() in order to have confirmation window (see upstream/master)
disputesService.resolveDisputePayout(dispute, disputeResult, contract);
doClose(closeTicketButton);
// if (dispute.getDepositTxSerialized() == null) {
@ -801,49 +760,12 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
return;
}
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
boolean isRefundAgent = disputeManager instanceof RefundManager;
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
disputeResult.setCloseDate(new Date());
dispute.setDisputeResult(disputeResult);
dispute.setIsClosed();
DisputeResult.Reason reason = disputeResult.getReason();
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
String role = isRefundAgent ? Res.get("shared.refundAgent") : Res.get("shared.mediator");
String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress();
Contract contract = dispute.getContract();
String currencyCode = contract.getOfferPayload().getCurrencyCode();
String amount = formatter.formatCoinWithCode(contract.getTradeAmount());
String textToSign = Res.get("disputeSummaryWindow.close.msg",
DisplayUtils.formatDateTime(disputeResult.getCloseDate()),
role,
agentNodeAddress,
dispute.getShortTradeId(),
currencyCode,
amount,
formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()),
formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()),
Res.get("disputeSummaryWindow.reason." + reason.name()),
disputeResult.summaryNotesProperty().get()
);
if (reason == DisputeResult.Reason.OPTION_TRADE &&
dispute.getChatMessages().size() > 1 &&
dispute.getChatMessages().get(1).isSystemMessage()) {
textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
}
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
if (isRefundAgent) {
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
} else {
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation");
}
disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText);
disputesService.closeDispute(disputeManager, dispute, disputeResult, isRefundAgent);
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
UserThread.runAfter(() -> new Popup()
@ -852,12 +774,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
200, TimeUnit.MILLISECONDS);
}
finalizeDisputeHandlerOptional.ifPresent(Runnable::run);
disputeManager.requestPersistence();
closeTicketButton.disableProperty().unbind();
hide();
}
@ -878,33 +796,24 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
}
private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) {
Contract contract = dispute.getContract();
Offer offer = new Offer(contract.getOfferPayload());
Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit();
Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit();
Coin tradeAmount = contract.getTradeAmount();
CoreDisputesService.DisputePayout payout;
if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) {
disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit));
disputeResult.setSellerPayoutAmount(sellerSecurityDeposit);
payout = CoreDisputesService.DisputePayout.BUYER_GETS_TRADE_AMOUNT;
disputeResult.setWinner(DisputeResult.Winner.BUYER);
} else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) {
disputeResult.setBuyerPayoutAmount(tradeAmount
.add(buyerSecurityDeposit)
.add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser (see post v1.1.7)
disputeResult.setSellerPayoutAmount(Coin.ZERO);
payout = CoreDisputesService.DisputePayout.BUYER_GETS_ALL;
disputeResult.setWinner(DisputeResult.Winner.BUYER);
} else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) {
disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit);
disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit));
payout = CoreDisputesService.DisputePayout.SELLER_GETS_TRADE_AMOUNT;
disputeResult.setWinner(DisputeResult.Winner.SELLER);
} else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) {
disputeResult.setBuyerPayoutAmount(Coin.ZERO);
disputeResult.setSellerPayoutAmount(tradeAmount
.add(sellerSecurityDeposit)
.add(buyerSecurityDeposit));
payout = CoreDisputesService.DisputePayout.SELLER_GETS_ALL;
disputeResult.setWinner(DisputeResult.Winner.SELLER);
} else {
// should not happen
throw new IllegalStateException("Unknown radio button");
}
disputesService.applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, -1);
buyerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getBuyerPayoutAmount()));
sellerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getSellerPayoutAmount()));
}

View file

@ -204,8 +204,8 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
rows++;
if (trade.getPayoutTx() != null)
rows++;
boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() &&
arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null;
boolean showDisputedTx = arbitrationManager.findDispute(trade.getId()).isPresent() &&
arbitrationManager.findDispute(trade.getId()).get().getDisputePayoutTxId() != null;
if (showDisputedTx)
rows++;
if (trade.hasFailed())
@ -301,7 +301,7 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
trade.getPayoutTx().getHash());
if (showDisputedTx)
addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"),
arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId());
arbitrationManager.findDispute(trade.getId()).get().getDisputePayoutTxId());
if (trade.hasFailed()) {
textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second;

View file

@ -18,7 +18,7 @@
package bisq.desktop.main.overlays.windows;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.main.support.dispute.DisputeSummaryVerification;
import bisq.core.support.dispute.DisputeSummaryVerification;
import bisq.core.locale.Res;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;

View file

@ -29,6 +29,7 @@ import bisq.desktop.main.support.dispute.client.mediation.MediationClientView;
import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.api.CoreDisputesService;
import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res;
@ -122,6 +123,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
private Trade selectedTrade;
@Getter
private final PubKeyRingProvider pubKeyRingProvider;
private final CoreDisputesService disputesService;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, initialization
@ -141,7 +143,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
Navigation navigation,
WalletPasswordWindow walletPasswordWindow,
NotificationCenter notificationCenter,
OfferUtil offerUtil) {
OfferUtil offerUtil,
CoreDisputesService disputesService) {
this.tradeManager = tradeManager;
this.xmrWalletService = xmrWalletService;
this.pubKeyRingProvider = pubKeyRingProvider;
@ -156,6 +159,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
this.walletPasswordWindow = walletPasswordWindow;
this.notificationCenter = notificationCenter;
this.offerUtil = offerUtil;
this.disputesService = disputesService;
tradesListChangeListener = change -> onListChanged();
notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId);
@ -544,40 +548,12 @@ public class PendingTradesDataModel extends ActivatableDataModel {
} else if (useArbitration) {
// Only if we have completed mediation we allow arbitration
disputeManager = arbitrationManager;
PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing();
checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null");
byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); TODO (woodser)
String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser)
Dispute dispute = new Dispute(new Date().getTime(),
trade.getId(),
pubKeyRingProvider.get().hashCode(), // trader id,
true,
(offer.getDirection() == OfferPayload.Direction.BUY) == isMaker,
isMaker,
pubKeyRingProvider.get(),
trade.getDate().getTime(),
trade.getMaxTradePeriodDate().getTime(),
trade.getContract(),
trade.getContractHash(),
depositTxSerialized,
payoutTxSerialized,
depositTxHashAsString,
payoutTxHashAsString,
trade.getContractAsJson(),
trade.getMaker().getContractSignature(),
trade.getTaker().getContractSignature(),
trade.getMaker().getPaymentAccountPayload(),
trade.getTaker().getPaymentAccountPayload(),
arbitratorPubKeyRing,
isSupportTicket,
SupportType.ARBITRATION);
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex);
tradeManager.requestPersistence();
} else {
log.warn("Invalid dispute state {}", disputeState.name());
}
tradeManager.requestPersistence();
}
private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {

View file

@ -486,7 +486,7 @@ public abstract class TradeStepView extends AnchorPane {
}
applyOnDisputeOpened();
ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId());
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED);
@ -499,7 +499,7 @@ public abstract class TradeStepView extends AnchorPane {
}
applyOnDisputeOpened();
ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId());
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
@ -513,7 +513,7 @@ public abstract class TradeStepView extends AnchorPane {
}
applyOnDisputeOpened();
ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId());
ownDispute = model.dataModel.mediationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED);
@ -525,7 +525,7 @@ public abstract class TradeStepView extends AnchorPane {
}
applyOnDisputeOpened();
ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId());
ownDispute = model.dataModel.mediationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null) {
tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_PEER_REQUESTED);

View file

@ -1,103 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.support.dispute;
import bisq.core.locale.Res;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.agent.DisputeAgent;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.CryptoException;
import bisq.common.crypto.Hash;
import bisq.common.crypto.Sig;
import bisq.common.util.Utilities;
import java.security.KeyPair;
import java.security.PublicKey;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class DisputeSummaryVerification {
// Must not change as it is used for splitting the text for verifying the signature of the summary message
private static final String SEPARATOR1 = "\n-----BEGIN SIGNATURE-----\n";
private static final String SEPARATOR2 = "\n-----END SIGNATURE-----\n";
public static String signAndApply(DisputeManager<? extends DisputeList<Dispute>> disputeManager,
DisputeResult disputeResult,
String textToSign) {
byte[] hash = Hash.getSha256Hash(textToSign);
KeyPair signatureKeyPair = disputeManager.getSignatureKeyPair();
String sigAsHex;
try {
byte[] signature = Sig.sign(signatureKeyPair.getPrivate(), hash);
sigAsHex = Utilities.encodeToHex(signature);
disputeResult.setArbitratorSignature(signature);
} catch (CryptoException e) {
sigAsHex = "Signing failed";
}
return Res.get("disputeSummaryWindow.close.msgWithSig",
textToSign,
SEPARATOR1,
sigAsHex,
SEPARATOR2);
}
public static String verifySignature(String input,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager) {
try {
String[] parts = input.split(SEPARATOR1);
String textToSign = parts[0];
String fullAddress = textToSign.split("\n")[1].split(": ")[1];
NodeAddress nodeAddress = new NodeAddress(fullAddress);
DisputeAgent disputeAgent = mediatorManager.getDisputeAgentByNodeAddress(nodeAddress).orElse(null);
if (disputeAgent == null) {
disputeAgent = refundAgentManager.getDisputeAgentByNodeAddress(nodeAddress).orElse(null);
}
checkNotNull(disputeAgent);
PublicKey pubKey = disputeAgent.getPubKeyRing().getSignaturePubKey();
String sigString = parts[1].split(SEPARATOR2)[0];
byte[] sig = Utilities.decodeFromHex(sigString);
byte[] hash = Hash.getSha256Hash(textToSign);
try {
boolean result = Sig.verify(pubKey, hash, sig);
if (result) {
return Res.get("support.sigCheck.popup.success");
} else {
return Res.get("support.sigCheck.popup.failed");
}
} catch (CryptoException e) {
return Res.get("support.sigCheck.popup.failed");
}
} catch (Throwable e) {
return Res.get("support.sigCheck.popup.invalidFormat");
}
}
}