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

@ -29,6 +29,9 @@ import bisq.core.offer.OfferPayload;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PaymentMethod;
import bisq.core.support.dispute.Attachment;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.statistics.TradeStatisticsManager;
@ -36,6 +39,7 @@ import bisq.common.app.Version;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.crypto.IncorrectPasswordException; import bisq.common.crypto.IncorrectPasswordException;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.FaultHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import bisq.proto.grpc.NotificationMessage; import bisq.proto.grpc.NotificationMessage;
@ -79,6 +83,7 @@ public class CoreApi {
private final AppStartupState appStartupState; private final AppStartupState appStartupState;
private final CoreAccountService coreAccountService; private final CoreAccountService coreAccountService;
private final CoreDisputeAgentsService coreDisputeAgentsService; private final CoreDisputeAgentsService coreDisputeAgentsService;
private final CoreDisputesService coreDisputeService;
private final CoreHelpService coreHelpService; private final CoreHelpService coreHelpService;
private final CoreOffersService coreOffersService; private final CoreOffersService coreOffersService;
private final CorePaymentAccountsService paymentAccountsService; private final CorePaymentAccountsService paymentAccountsService;
@ -94,6 +99,7 @@ public class CoreApi {
AppStartupState appStartupState, AppStartupState appStartupState,
CoreAccountService coreAccountService, CoreAccountService coreAccountService,
CoreDisputeAgentsService coreDisputeAgentsService, CoreDisputeAgentsService coreDisputeAgentsService,
CoreDisputesService coreDisputeService,
CoreHelpService coreHelpService, CoreHelpService coreHelpService,
CoreOffersService coreOffersService, CoreOffersService coreOffersService,
CorePaymentAccountsService paymentAccountsService, CorePaymentAccountsService paymentAccountsService,
@ -107,6 +113,7 @@ public class CoreApi {
this.appStartupState = appStartupState; this.appStartupState = appStartupState;
this.coreAccountService = coreAccountService; this.coreAccountService = coreAccountService;
this.coreDisputeAgentsService = coreDisputeAgentsService; this.coreDisputeAgentsService = coreDisputeAgentsService;
this.coreDisputeService = coreDisputeService;
this.coreHelpService = coreHelpService; this.coreHelpService = coreHelpService;
this.coreOffersService = coreOffersService; this.coreOffersService = coreOffersService;
this.paymentAccountsService = paymentAccountsService; this.paymentAccountsService = paymentAccountsService;
@ -333,6 +340,31 @@ public class CoreApi {
notificationService.sendNotification(notification); notificationService.sendNotification(notification);
} }
///////////////////////////////////////////////////////////////////////////////////////////
// Disputes
///////////////////////////////////////////////////////////////////////////////////////////
public List<Dispute> getDisputes() {
return coreDisputeService.getDisputes();
}
public Dispute getDispute(String tradeId) {
return coreDisputeService.getDispute(tradeId);
}
public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) {
coreDisputeService.openDispute(tradeId, resultHandler, faultHandler);
}
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason,
String summaryNotes, long customPayoutAmount) {
coreDisputeService.resolveDispute(tradeId, winner, reason, summaryNotes, customPayoutAmount);
}
public void sendDisputeChatMessage(String disputeId, String message, ArrayList<Attachment> attachments) {
coreDisputeService.sendDisputeChatMessage(disputeId, message, attachments);
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Dispute Agents // Dispute Agents
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -0,0 +1,335 @@
package bisq.core.api;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.support.SupportType;
import bisq.core.support.dispute.Attachment;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.DisputeSummaryVerification;
import bisq.core.support.dispute.arbitration.ArbitrationManager;
import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing;
import bisq.common.handlers.FaultHandler;
import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import com.google.inject.name.Named;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
@Singleton
@Slf4j
public class CoreDisputesService {
public enum DisputePayout {
BUYER_GETS_TRADE_AMOUNT,
BUYER_GETS_ALL, // used in desktop
SELLER_GETS_TRADE_AMOUNT,
SELLER_GETS_ALL, // used in desktop
CUSTOM
}
private final ArbitrationManager arbitrationManager;
private final CoinFormatter formatter;
private final KeyRing keyRing;
private final TradeManager tradeManager;
private final XmrWalletService xmrWalletService;
@Inject
public CoreDisputesService(ArbitrationManager arbitrationManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, // TODO: XMR?
KeyRing keyRing,
TradeManager tradeManager,
XmrWalletService xmrWalletService) {
this.arbitrationManager = arbitrationManager;
this.formatter = formatter;
this.keyRing = keyRing;
this.tradeManager = tradeManager;
this.xmrWalletService = xmrWalletService;
}
public List<Dispute> getDisputes() {
return arbitrationManager.getDisputesAsObservableList();
}
public Dispute getDispute(String tradeId) {
Optional<Dispute> dispute = arbitrationManager.findDispute(tradeId);
if (dispute.isPresent()) return dispute.get();
else throw new IllegalStateException(format("dispute for trade id '%s' not found", tradeId));
}
public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) {
Trade trade = tradeManager.getTradeById(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
Offer offer = trade.getOffer();
if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId));
// Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded
// to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored.
var disputeManager = arbitrationManager;
var isSupportTicket = false;
var isMaker = tradeManager.isMyOffer(offer);
var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket);
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
String updatedMultisigHex = multisigWallet.getMultisigHex();
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
tradeManager.requestPersistence();
}
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null;
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(),
pubKey.hashCode(), // trader id,
true,
(offer.getDirection() == OfferPayload.Direction.BUY) == isMaker,
isMaker,
pubKey,
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);
return dispute;
}
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
try {
var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
.filter(d -> tradeId.equals(d.getTradeId()))
.findFirst();
Dispute dispute;
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
var closeDate = new Date();
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate);
var contract = dispute.getContract();
DisputePayout payout;
if (customWinnerAmount > 0) {
payout = DisputePayout.CUSTOM;
} else if (winner == DisputeResult.Winner.BUYER) {
payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT;
} else if (winner == DisputeResult.Winner.SELLER) {
payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT;
} else {
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
}
applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount);
// resolve the payout
resolveDisputePayout(dispute, disputeResult, contract);
// close dispute ticket
closeDispute(arbitrationManager, dispute, disputeResult, false);
// close dispute ticket for peer
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
.filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId())
.findFirst();
if (peersDisputeOptional.isPresent()) {
var peerDispute = peersDisputeOptional.get();
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher());
resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract());
closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false);
} else {
throw new IllegalStateException("could not find peer dispute");
}
arbitrationManager.requestPersistence();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private DisputeResult createDisputeResult(Dispute dispute, DisputeResult.Winner winner, DisputeResult.Reason reason,
String summaryNotes, Date closeDate) {
var disputeResult = new DisputeResult(dispute.getTradeId(), dispute.getTraderId());
disputeResult.setWinner(winner);
disputeResult.setReason(reason);
disputeResult.setSummaryNotes(summaryNotes);
disputeResult.setCloseDate(closeDate);
return disputeResult;
}
/**
* Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer
* receives the remaining amount minus the mining fee.
*/
public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
Contract contract = dispute.getContract();
Offer offer = new Offer(contract.getOfferPayload());
Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit();
Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit();
Coin tradeAmount = contract.getTradeAmount();
if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit));
disputeResult.setSellerPayoutAmount(sellerSecurityDeposit);
} else if (payout == DisputePayout.BUYER_GETS_ALL) {
disputeResult.setBuyerPayoutAmount(tradeAmount
.add(buyerSecurityDeposit)
.add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser (see post v1.1.7)
disputeResult.setSellerPayoutAmount(Coin.ZERO);
} else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit);
disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit));
} else if (payout == DisputePayout.SELLER_GETS_ALL) {
disputeResult.setBuyerPayoutAmount(Coin.ZERO);
disputeResult.setSellerPayoutAmount(tradeAmount
.add(sellerSecurityDeposit)
.add(buyerSecurityDeposit));
} else if (payout == DisputePayout.CUSTOM) {
Coin winnerAmount = Coin.valueOf(customWinnerAmount);
Coin loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).minus(winnerAmount);
disputeResult.setBuyerPayoutAmount(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? winnerAmount : loserAmount);
disputeResult.setSellerPayoutAmount(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : winnerAmount);
}
}
public void resolveDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) {
// 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 = xmrWalletService.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;
}
}
}
// From DisputeSummaryWindow.java
public void closeDispute(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, boolean isRefundAgent) {
dispute.setDisputeResult(disputeResult);
dispute.setIsClosed();
DisputeResult.Reason reason = disputeResult.getReason();
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",
FormattingUtils.formatDateTime(disputeResult.getCloseDate(), true),
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);
}
public void sendDisputeChatMessage(String disputeId, String message, ArrayList<Attachment> attachments) {
var disputeOptional = arbitrationManager.findDisputeById(disputeId);
Dispute dispute;
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
else throw new IllegalStateException(format("dispute with id '%s' not found", disputeId));
ChatMessage chatMessage = new ChatMessage(
arbitrationManager.getSupportType(),
dispute.getTradeId(),
dispute.getTraderId(),
arbitrationManager.isTrader(dispute),
message,
arbitrationManager.getMyAddress(),
attachments);
dispute.addAndPersistChatMessage(chatMessage);
arbitrationManager.sendChatMessage(chatMessage);
}
}

View file

@ -3,6 +3,7 @@ package bisq.core.api;
import bisq.core.api.CoreApi.NotificationListener; import bisq.core.api.CoreApi.NotificationListener;
import bisq.core.api.model.TradeInfo; import bisq.core.api.model.TradeInfo;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.support.messages.ChatMessage;
import bisq.proto.grpc.NotificationMessage; import bisq.proto.grpc.NotificationMessage;
import bisq.proto.grpc.NotificationMessage.NotificationType; import bisq.proto.grpc.NotificationMessage.NotificationType;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -56,4 +57,12 @@ public class CoreNotificationService {
.setTitle(title) .setTitle(title)
.setMessage(message).build()); .setMessage(message).build());
} }
public void sendChatNotification(ChatMessage chatMessage) {
sendNotification(NotificationMessage.newBuilder()
.setType(NotificationType.CHAT_MESSAGE)
.setTimestamp(System.currentTimeMillis())
.setChatMessage(chatMessage.toProtoChatMessageBuilder())
.build());
}
} }

View file

@ -18,6 +18,7 @@
package bisq.core.support; package bisq.core.support;
import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.api.CoreNotificationService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.support.messages.SupportMessage; import bisq.core.support.messages.SupportMessage;
@ -48,6 +49,7 @@ import javax.annotation.Nullable;
public abstract class SupportManager { public abstract class SupportManager {
protected final P2PService p2PService; protected final P2PService p2PService;
protected final CoreMoneroConnectionsService connectionService; protected final CoreMoneroConnectionsService connectionService;
protected final CoreNotificationService notificationService;
protected final Map<String, Timer> delayMsgMap = new HashMap<>(); protected final Map<String, Timer> delayMsgMap = new HashMap<>();
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
@ -59,10 +61,11 @@ public abstract class SupportManager {
// Constructor // Constructor
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService) { public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService, CoreNotificationService notificationService) {
this.p2PService = p2PService; this.p2PService = p2PService;
this.connectionService = connectionService; this.connectionService = connectionService;
mailboxMessageService = p2PService.getMailboxMessageService(); this.mailboxMessageService = p2PService.getMailboxMessageService();
this.notificationService = notificationService;
// We get first the message handler called then the onBootstrapped // We get first the message handler called then the onBootstrapped
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
@ -152,6 +155,7 @@ public abstract class SupportManager {
PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage); PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage);
addAndPersistChatMessage(chatMessage); addAndPersistChatMessage(chatMessage);
notificationService.sendChatNotification(chatMessage);
// We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we // We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we
// cannot send it anyway) // cannot send it anyway)

View file

@ -18,6 +18,7 @@
package bisq.core.support.dispute; package bisq.core.support.dispute;
import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.api.CoreNotificationService;
import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.Restrictions;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
@ -111,6 +112,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
TradeWalletService tradeWalletService, TradeWalletService tradeWalletService,
XmrWalletService xmrWalletService, XmrWalletService xmrWalletService,
CoreMoneroConnectionsService connectionService, CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
@ -118,7 +120,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
DisputeListService<T> disputeListService, DisputeListService<T> disputeListService,
Config config, Config config,
PriceFeedService priceFeedService) { PriceFeedService priceFeedService) {
super(p2PService, connectionService); super(p2PService, connectionService, notificationService);
this.tradeWalletService = tradeWalletService; this.tradeWalletService = tradeWalletService;
this.xmrWalletService = xmrWalletService; this.xmrWalletService = xmrWalletService;
@ -288,17 +290,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return pubKeyRing.equals(dispute.getTraderPubKeyRing()); return pubKeyRing.equals(dispute.getTraderPubKeyRing());
} }
public Optional<Dispute> findOwnDispute(String tradeId) {
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
return Optional.empty();
}
return disputeList.stream().filter(e -> e.getTradeId().equals(tradeId)).findAny();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Message handler // Message handler
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -823,6 +814,17 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
.findAny(); .findAny();
} }
public Optional<Dispute> findDisputeById(String disputeId) {
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
return Optional.empty();
}
return disputeList.stream()
.filter(e -> e.getId().equals(disputeId))
.findAny();
}
public Optional<Trade> findTrade(Dispute dispute) { public Optional<Trade> findTrade(Dispute dispute) {
Optional<Trade> retVal = tradeManager.getTradeById(dispute.getTradeId()); Optional<Trade> retVal = tradeManager.getTradeById(dispute.getTradeId());
if (!retVal.isPresent()) { if (!retVal.isPresent()) {

View file

@ -15,13 +15,9 @@
* along with Haveno. If not, see <http://www.gnu.org/licenses/>. * along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/ */
package bisq.desktop.main.support.dispute; package bisq.core.support.dispute;
import bisq.core.locale.Res; 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.agent.DisputeAgent;
import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;

View file

@ -18,6 +18,7 @@
package bisq.core.support.dispute.arbitration; package bisq.core.support.dispute.arbitration;
import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.api.CoreNotificationService;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
@ -60,8 +61,6 @@ import bisq.common.crypto.PubKeyRing;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import com.google.common.base.Preconditions;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
@ -96,6 +95,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
TradeWalletService tradeWalletService, TradeWalletService tradeWalletService,
XmrWalletService walletService, XmrWalletService walletService,
CoreMoneroConnectionsService connectionService, CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
@ -103,7 +103,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
ArbitrationDisputeListService arbitrationDisputeListService, ArbitrationDisputeListService arbitrationDisputeListService,
Config config, Config config,
PriceFeedService priceFeedService) { PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService); openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService);
} }
@ -286,7 +286,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
MoneroTxSet txSet = traderSignsDisputePayoutTx(tradeId, arbitratorSignedPayoutTxHex); MoneroTxSet txSet = traderSignsDisputePayoutTx(tradeId, arbitratorSignedPayoutTxHex);
onTraderSignedDisputePayoutTx(tradeId, txSet); onTraderSignedDisputePayoutTx(tradeId, txSet);
} catch (Exception e) { } catch (Exception e) {
errorMessage = "Failed to sign dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId; e.printStackTrace();
errorMessage = "Failed to sign dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId + " SignedPayoutTx = " + arbitratorSignedPayoutTxHex;
log.warn(errorMessage); log.warn(errorMessage);
success = false; success = false;
} }
@ -318,7 +319,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// } // }
// catch (AddressFormatException | WalletException e) { // catch (AddressFormatException | WalletException e) {
catch (Exception e) { catch (Exception e) {
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString(); errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx: " + e.toString();
log.error(errorMessage, e); log.error(errorMessage, e);
success = false; success = false;
@ -343,7 +344,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) { private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
String uid = peerPublishedDisputePayoutTxMessage.getUid(); String uid = peerPublishedDisputePayoutTxMessage.getUid();
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId(); String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
Optional<Dispute> disputeOptional = findOwnDispute(tradeId); Optional<Dispute> disputeOptional = findDispute(tradeId);
if (!disputeOptional.isPresent()) { if (!disputeOptional.isPresent()) {
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId); log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) { if (!delayMsgMap.containsKey(uid)) {
@ -473,7 +474,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// gather trade info // gather trade info
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
Optional<Dispute> disputeOptional = findOwnDispute(tradeId); Optional<Dispute> disputeOptional = findDispute(tradeId);
if (!disputeOptional.isPresent()) { if (!disputeOptional.isPresent()) {
log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
return; return;
@ -579,78 +580,46 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// Disputed payout tx signing // Disputed payout tx signing
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): where to move this common logic?
public static MoneroTxWallet arbitratorCreatesDisputedPayoutTx(Contract contract, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) { public static MoneroTxWallet arbitratorCreatesDisputedPayoutTx(Contract contract, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) {
//System.out.println("DisputeSummaryWindow.arbitratorSignsDisputedPayoutTx()"); // multisig wallet must be synced
//System.out.println("=== DISPUTE ==="); if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx");
//System.out.println(dispute);
//System.out.println("=== CONTRACT ===");
//System.out.println(contract); // TODO (woodser): contract should include deposit tx hashes (pre-created then hash shared then contract signed)
//System.out.println("=== DISPUTE RESULT ===");
//System.out.println(disputeResult);
// gather relevant trade info // collect winner and loser payout address and amounts
String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString(); String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); (contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
Preconditions.checkNotNull(buyerPayoutAddress, "buyerPayoutAddress must not be null"); (contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
Preconditions.checkNotNull(sellerPayoutAddress, "sellerPayoutAddress must not be null"); String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
BigInteger buyerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getBuyerPayoutAmount()); BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger sellerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getSellerPayoutAmount()); BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
//System.out.println("buyerPayoutAddress: " + buyerPayoutAddress);
//System.out.println("buyerPayoutAmount: " + buyerPayoutAmount);
// Offer offer = new Offer(contract.getOfferPayload());
// System.out.println("Buyer deposit tx fee: " +
//System.out.println("sellerPayoutAddress: " + sellerPayoutAddress);
//System.out.println("sellerPayoutAmount: " + sellerPayoutAmount);
//System.out.println("Multisig balance: " + multisigWallet.getBalance());
//System.out.println("Multisig unlocked balance: " + multisigWallet.getUnlockedBalance());
//System.out.println("Multisig txs");
//System.out.println(multisigWallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)));
// create transaction to get fee estimate // create transaction to get fee estimate
if (multisigWallet.isMultisigImportNeeded()) {
log.info("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx");
return null;
}
// TODO (woodser): include arbitration fee // TODO (woodser): include arbitration fee
//System.out.println("Creating feeEstimateTx!");
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (buyerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5))); // reduce payment amount to compute fee of similar tx if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx
if (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5))); if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig); MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig);
System.out.println("Created fee estimate tx!"); // create payout tx by increasing estimated fee until successful
System.out.println(feeEstimateTx);
//BigInteger estimatedFee = feeEstimateTx.getFee();
// attempt to create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null; MoneroTxWallet payoutTx = null;
int numAttempts = 0; int numAttempts = 0;
int feeDivisor = 0; // adjust fee divisor based on number of payout destinations
if (buyerPayoutAmount.compareTo(BigInteger.ZERO) == 1) feeDivisor += 1;
if (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) feeDivisor += 1;
while (payoutTx == null && numAttempts < 50) { while (payoutTx == null && numAttempts < 50) {
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10 of fee until tx is successful BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (buyerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(feeDivisor)))); // split fee subtracted from each payout amount if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0
if (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(feeDivisor)))); if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
try { if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee");
if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee
}
numAttempts++; numAttempts++;
try {
payoutTx = multisigWallet.createTx(txConfig); payoutTx = multisigWallet.createTx(txConfig);
} catch (MoneroError e) { } catch (MoneroError e) {
// exception expected // TODO: better way of estimating fee? // exception expected // TODO: better way of estimating fee?
} }
} }
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx"); log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
System.out.println("DISPUTE PAYOUT TX GENERATED ON ATTEMPT " + numAttempts);
System.out.println(payoutTx);
return payoutTx; return payoutTx;
} }
@ -658,7 +627,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// gather trade info // gather trade info
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
Optional<Dispute> disputeOptional = findOwnDispute(tradeId); Optional<Dispute> disputeOptional = findDispute(tradeId);
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
Dispute dispute = disputeOptional.get(); Dispute dispute = disputeOptional.get();
Contract contract = dispute.getContract(); Contract contract = dispute.getContract();
@ -668,10 +637,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids // BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids
// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount(); // BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount();
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER); // BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
BigInteger buyerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getBuyerPayoutAmount());
BigInteger sellerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getSellerPayoutAmount());
System.out.println("Buyer payout amount (with multiplier): " + buyerPayoutAmount);
System.out.println("Seller payout amount (with multiplier): " + sellerPayoutAmount);
// parse arbitrator-signed payout tx // parse arbitrator-signed payout tx
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
@ -694,41 +659,31 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract"); if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
// verify change address is multisig's primary address // verify change address is multisig's primary address
if (!arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address"); if (arbitratorSignedPayoutTx.getChangeAddress() != null && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
// verify sum of outputs = destination amounts + change amount // verify sum of outputs = destination amounts + change amount
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount()); BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount"); if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
// verify buyer destination amount is payout amount - 1/2 tx costs
if (buyerPayoutDestination != null) {
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount());
BigInteger expectedBuyerPayout = buyerPayoutAmount.subtract(txCost.divide(BigInteger.valueOf(2)));
System.out.println("Dispute buyer payout amount: " + buyerPayoutAmount);
System.out.println("Tx cost: " + txCost);
System.out.println("Buyer destination payout amount: " + buyerPayoutDestination.getAmount());
}
// payout amount is dispute payout amount - 1/2 tx cost - deposit tx fee
// TODO (woodser): VERIFY PAYOUT TX AMOUNTS WHICH CONSIDERS FEE IF LONG TRADE, EXACT AMOUNT IF SHORT TRADE
// if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new RuntimeException("Buyer destination amount is not payout amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
// verify seller destination amount is payout amount - 1/2 tx costs
// BigInteger expectedSellerPayout = sellerPayoutAmount.subtract(txCost.divide(BigInteger.valueOf(2)));
// if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new RuntimeException("Seller destination amount is not payout amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx) // TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
// verify winner and loser payout amounts
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount();
BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount();
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
// update multisig wallet from arbitrator // update multisig wallet from arbitrator
System.out.println("Updating multisig hex from arbitrator: " + disputeResult.getArbitratorUpdatedMultisigHex()); System.out.println("Updating multisig hex from arbitrator");
multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex())); multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex()));
// sign arbitrator-signed payout tx // sign arbitrator-signed payout tx
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex); MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex(); String signedMultisigTxHex = result.getSignedMultisigTxHex();
parsedTxSet.setMultisigTxHex(signedMultisigTxHex); parsedTxSet.setMultisigTxHex(signedMultisigTxHex);

View file

@ -18,6 +18,7 @@
package bisq.core.support.dispute.mediation; package bisq.core.support.dispute.mediation;
import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.api.CoreNotificationService;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
@ -78,6 +79,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
TradeWalletService tradeWalletService, TradeWalletService tradeWalletService,
XmrWalletService walletService, XmrWalletService walletService,
CoreMoneroConnectionsService connectionService, CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
@ -85,7 +87,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
MediationDisputeListService mediationDisputeListService, MediationDisputeListService mediationDisputeListService,
Config config, Config config,
PriceFeedService priceFeedService) { PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService); openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService);
} }

View file

@ -18,6 +18,7 @@
package bisq.core.support.dispute.refund; package bisq.core.support.dispute.refund;
import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.api.CoreNotificationService;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
@ -72,6 +73,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
TradeWalletService tradeWalletService, TradeWalletService tradeWalletService,
XmrWalletService walletService, XmrWalletService walletService,
CoreMoneroConnectionsService connectionService, CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
@ -80,7 +82,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
RefundDisputeListService refundDisputeListService, RefundDisputeListService refundDisputeListService,
Config config, Config config,
PriceFeedService priceFeedService) { PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
openOfferManager, keyRing, refundDisputeListService, config, priceFeedService); openOfferManager, keyRing, refundDisputeListService, config, priceFeedService);
} }

View file

@ -205,9 +205,7 @@ public final class ChatMessage extends SupportMessage {
notifyChangeListener(); notifyChangeListener();
} }
// We cannot rename protobuf definition because it would break backward compatibility public protobuf.ChatMessage.Builder toProtoChatMessageBuilder() {
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder() protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder()
.setType(SupportType.toProtoMessage(supportType)) .setType(SupportType.toProtoMessage(supportType))
.setTradeId(tradeId) .setTradeId(tradeId)
@ -225,6 +223,14 @@ public final class ChatMessage extends SupportMessage {
.setWasDisplayed(wasDisplayed); .setWasDisplayed(wasDisplayed);
Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError); Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError);
Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError); Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError);
return builder;
}
// We cannot rename protobuf definition because it would break backward compatibility
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.ChatMessage.Builder builder = toProtoChatMessageBuilder();
return getNetworkEnvelopeBuilder() return getNetworkEnvelopeBuilder()
.setChatMessage(builder) .setChatMessage(builder)
.build(); .build();

View file

@ -21,8 +21,6 @@ import bisq.core.support.SupportSession;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.common.crypto.PubKeyRing;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;

View file

@ -18,6 +18,7 @@
package bisq.core.support.traderchat; package bisq.core.support.traderchat;
import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.api.CoreNotificationService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.support.SupportManager; import bisq.core.support.SupportManager;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
@ -57,9 +58,10 @@ public class TraderChatManager extends SupportManager {
@Inject @Inject
public TraderChatManager(P2PService p2PService, public TraderChatManager(P2PService p2PService,
CoreMoneroConnectionsService connectionService, CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager, TradeManager tradeManager,
PubKeyRingProvider pubKeyRingProvider) { PubKeyRingProvider pubKeyRingProvider) {
super(p2PService, connectionService); super(p2PService, connectionService, notificationService);
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
this.pubKeyRingProvider = pubKeyRingProvider; this.pubKeyRingProvider = pubKeyRingProvider;
} }

View file

@ -21,6 +21,7 @@ public class ParsingUtils {
* Multiplier to convert centineros (the base XMR unit of Coin) to atomic units. * Multiplier to convert centineros (the base XMR unit of Coin) to atomic units.
* *
* TODO: change base unit to atomic units and long * TODO: change base unit to atomic units and long
* TODO: move these static utilities?
*/ */
private static BigInteger CENTINEROS_AU_MULTIPLIER = BigInteger.valueOf(10000); private static BigInteger CENTINEROS_AU_MULTIPLIER = BigInteger.valueOf(10000);

View file

@ -0,0 +1,156 @@
package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.core.support.dispute.Attachment;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.util.ParsingUtils;
import bisq.common.proto.ProtoUtil;
import bisq.proto.grpc.DisputesGrpc.DisputesImplBase;
import bisq.proto.grpc.GetDisputeReply;
import bisq.proto.grpc.GetDisputeRequest;
import bisq.proto.grpc.GetDisputesReply;
import bisq.proto.grpc.GetDisputesRequest;
import bisq.proto.grpc.OpenDisputeReply;
import bisq.proto.grpc.OpenDisputeRequest;
import bisq.proto.grpc.ResolveDisputeReply;
import bisq.proto.grpc.ResolveDisputeRequest;
import bisq.proto.grpc.SendDisputeChatMessageReply;
import bisq.proto.grpc.SendDisputeChatMessageRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static bisq.proto.grpc.DisputesGrpc.getGetDisputeMethod;
import static bisq.proto.grpc.DisputesGrpc.getGetDisputesMethod;
import static bisq.proto.grpc.DisputesGrpc.getOpenDisputeMethod;
import static bisq.proto.grpc.DisputesGrpc.getResolveDisputeMethod;
import static bisq.proto.grpc.DisputesGrpc.getSendDisputeChatMessageMethod;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@Slf4j
public class GrpcDisputesService extends DisputesImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcDisputesService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
public void openDispute(OpenDisputeRequest req, StreamObserver<OpenDisputeReply> responseObserver) {
try {
coreApi.openDispute(req.getTradeId(),
() -> {
var reply = OpenDisputeReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
},
(errorMessage, throwable) -> {
log.info("Error in openDispute" + errorMessage);
exceptionHandler.handleException(log, throwable, responseObserver);
});
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void getDispute(GetDisputeRequest req, StreamObserver<GetDisputeReply> responseObserver) {
try {
var dispute = coreApi.getDispute(req.getTradeId());
var reply = GetDisputeReply.newBuilder()
.setDispute(dispute.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void getDisputes(GetDisputesRequest req, StreamObserver<GetDisputesReply> responseObserver) {
try {
var disputes = coreApi.getDisputes();
var disputesProtobuf = disputes.stream()
.map(d -> d.toProtoMessage())
.collect(Collectors.toList());
var reply = GetDisputesReply.newBuilder()
.addAllDisputes(disputesProtobuf)
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void resolveDispute(ResolveDisputeRequest req, StreamObserver<ResolveDisputeReply> responseObserver) {
try {
var winner = ProtoUtil.enumFromProto(DisputeResult.Winner.class, req.getWinner().name());
var reason = ProtoUtil.enumFromProto(DisputeResult.Reason.class, req.getReason().name());
// scale atomic unit to centineros for consistency TODO switch base to atomic units?
var customPayoutAmount = ParsingUtils.atomicUnitsToCentineros(req.getCustomPayoutAmount());
coreApi.resolveDispute(req.getTradeId(), winner, reason, req.getSummaryNotes(), customPayoutAmount);
var reply = ResolveDisputeReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void sendDisputeChatMessage(SendDisputeChatMessageRequest req,
StreamObserver<SendDisputeChatMessageReply> responseObserver) {
try {
var attachmentsProto = req.getAttachmentsList();
var attachments = attachmentsProto.stream().map(a -> Attachment.fromProto(a))
.collect(Collectors.toList());
coreApi.sendDisputeChatMessage(req.getDisputeId(), req.getMessage(), new ArrayList(attachments));
var reply = SendDisputeChatMessageReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{
put(getGetDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
}}
)));
}
}

View file

@ -51,6 +51,7 @@ public class GrpcServer {
PasswordAuthInterceptor passwordAuthInterceptor, PasswordAuthInterceptor passwordAuthInterceptor,
GrpcAccountService accountService, GrpcAccountService accountService,
GrpcDisputeAgentsService disputeAgentsService, GrpcDisputeAgentsService disputeAgentsService,
GrpcDisputesService disputesService,
GrpcHelpService helpService, GrpcHelpService helpService,
GrpcOffersService offersService, GrpcOffersService offersService,
GrpcPaymentAccountsService paymentAccountsService, GrpcPaymentAccountsService paymentAccountsService,
@ -66,6 +67,7 @@ public class GrpcServer {
.executor(UserThread.getExecutor()) .executor(UserThread.getExecutor())
.addService(interceptForward(accountService, accountService.interceptors())) .addService(interceptForward(accountService, accountService.interceptors()))
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors())) .addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
.addService(interceptForward(disputesService, disputesService.interceptors()))
.addService(interceptForward(helpService, helpService.interceptors())) .addService(interceptForward(helpService, helpService.interceptors()))
.addService(interceptForward(offersService, offersService.interceptors())) .addService(interceptForward(offersService, offersService.interceptors()))
.addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors())) .addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors()))

View file

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

View file

@ -23,7 +23,8 @@ import bisq.desktop.components.HavenoTextArea;
import bisq.desktop.components.InputTextField; import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.main.overlays.popups.Popup; 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.DisplayUtils;
import bisq.desktop.util.Layout; import bisq.desktop.util.Layout;
@ -49,7 +50,6 @@ import bisq.common.handlers.ResultHandler;
import bisq.common.util.Tuple2; import bisq.common.util.Tuple2;
import bisq.common.util.Tuple3; import bisq.common.util.Tuple3;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import com.google.inject.Inject; 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 bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
@Slf4j @Slf4j
public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> { public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private final CoinFormatter formatter; private final CoinFormatter formatter;
@ -98,8 +93,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private final MediationManager mediationManager; private final MediationManager mediationManager;
private final XmrWalletService walletService; 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 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 Dispute dispute;
private Optional<Runnable> finalizeDisputeHandlerOptional = Optional.empty();
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
private DisputeResult disputeResult; private DisputeResult disputeResult;
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton, private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
@ -132,13 +127,15 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
ArbitrationManager arbitrationManager, ArbitrationManager arbitrationManager,
MediationManager mediationManager, MediationManager mediationManager,
XmrWalletService walletService, XmrWalletService walletService,
TradeWalletService tradeWalletService) { TradeWalletService tradeWalletService,
CoreDisputesService disputesService) {
this.formatter = formatter; this.formatter = formatter;
this.arbitrationManager = arbitrationManager; this.arbitrationManager = arbitrationManager;
this.mediationManager = mediationManager; this.mediationManager = mediationManager;
this.walletService = walletService; this.walletService = walletService;
this.tradeWalletService = tradeWalletService; this.tradeWalletService = tradeWalletService;
this.disputesService = disputesService;
type = Type.Confirmation; 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 // Protected
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -598,39 +589,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
Button cancelButton = tuple.second; Button cancelButton = tuple.second;
closeTicketButton.setOnAction(e -> { closeTicketButton.setOnAction(e -> {
disputesService.resolveDisputePayout(dispute, disputeResult, contract);
// 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)
doClose(closeTicketButton); doClose(closeTicketButton);
// if (dispute.getDepositTxSerialized() == null) { // if (dispute.getDepositTxSerialized() == null) {
@ -801,49 +760,12 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
return; return;
} }
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
boolean isRefundAgent = disputeManager instanceof RefundManager; boolean isRefundAgent = disputeManager instanceof RefundManager;
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
disputeResult.setCloseDate(new Date()); disputeResult.setCloseDate(new Date());
dispute.setDisputeResult(disputeResult); disputesService.closeDispute(disputeManager, dispute, disputeResult, isRefundAgent);
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);
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
UserThread.runAfter(() -> new Popup() UserThread.runAfter(() -> new Popup()
@ -852,12 +774,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
200, TimeUnit.MILLISECONDS); 200, TimeUnit.MILLISECONDS);
} }
finalizeDisputeHandlerOptional.ifPresent(Runnable::run);
disputeManager.requestPersistence(); disputeManager.requestPersistence();
closeTicketButton.disableProperty().unbind(); closeTicketButton.disableProperty().unbind();
hide(); hide();
} }
@ -878,33 +796,24 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
} }
private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) { private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) {
Contract contract = dispute.getContract(); CoreDisputesService.DisputePayout payout;
Offer offer = new Offer(contract.getOfferPayload());
Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit();
Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit();
Coin tradeAmount = contract.getTradeAmount();
if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) { if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) {
disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit)); payout = CoreDisputesService.DisputePayout.BUYER_GETS_TRADE_AMOUNT;
disputeResult.setSellerPayoutAmount(sellerSecurityDeposit);
disputeResult.setWinner(DisputeResult.Winner.BUYER); disputeResult.setWinner(DisputeResult.Winner.BUYER);
} else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) { } else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) {
disputeResult.setBuyerPayoutAmount(tradeAmount payout = CoreDisputesService.DisputePayout.BUYER_GETS_ALL;
.add(buyerSecurityDeposit)
.add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser (see post v1.1.7)
disputeResult.setSellerPayoutAmount(Coin.ZERO);
disputeResult.setWinner(DisputeResult.Winner.BUYER); disputeResult.setWinner(DisputeResult.Winner.BUYER);
} else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) { } else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) {
disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit); payout = CoreDisputesService.DisputePayout.SELLER_GETS_TRADE_AMOUNT;
disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit));
disputeResult.setWinner(DisputeResult.Winner.SELLER); disputeResult.setWinner(DisputeResult.Winner.SELLER);
} else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) { } else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) {
disputeResult.setBuyerPayoutAmount(Coin.ZERO); payout = CoreDisputesService.DisputePayout.SELLER_GETS_ALL;
disputeResult.setSellerPayoutAmount(tradeAmount
.add(sellerSecurityDeposit)
.add(buyerSecurityDeposit));
disputeResult.setWinner(DisputeResult.Winner.SELLER); 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())); buyerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getBuyerPayoutAmount()));
sellerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getSellerPayoutAmount())); sellerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getSellerPayoutAmount()));
} }

View file

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

View file

@ -18,7 +18,7 @@
package bisq.desktop.main.overlays.windows; package bisq.desktop.main.overlays.windows;
import bisq.desktop.main.overlays.Overlay; 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.locale.Res;
import bisq.core.support.dispute.mediation.mediator.MediatorManager; 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.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.api.CoreDisputesService;
import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
@ -122,6 +123,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
private Trade selectedTrade; private Trade selectedTrade;
@Getter @Getter
private final PubKeyRingProvider pubKeyRingProvider; private final PubKeyRingProvider pubKeyRingProvider;
private final CoreDisputesService disputesService;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, initialization // Constructor, initialization
@ -141,7 +143,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
Navigation navigation, Navigation navigation,
WalletPasswordWindow walletPasswordWindow, WalletPasswordWindow walletPasswordWindow,
NotificationCenter notificationCenter, NotificationCenter notificationCenter,
OfferUtil offerUtil) { OfferUtil offerUtil,
CoreDisputesService disputesService) {
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
this.xmrWalletService = xmrWalletService; this.xmrWalletService = xmrWalletService;
this.pubKeyRingProvider = pubKeyRingProvider; this.pubKeyRingProvider = pubKeyRingProvider;
@ -156,6 +159,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
this.walletPasswordWindow = walletPasswordWindow; this.walletPasswordWindow = walletPasswordWindow;
this.notificationCenter = notificationCenter; this.notificationCenter = notificationCenter;
this.offerUtil = offerUtil; this.offerUtil = offerUtil;
this.disputesService = disputesService;
tradesListChangeListener = change -> onListChanged(); tradesListChangeListener = change -> onListChanged();
notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId); notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId);
@ -544,40 +548,12 @@ public class PendingTradesDataModel extends ActivatableDataModel {
} else if (useArbitration) { } else if (useArbitration) {
// Only if we have completed mediation we allow arbitration // Only if we have completed mediation we allow arbitration
disputeManager = arbitrationManager; disputeManager = arbitrationManager;
PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing(); Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
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);
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex); sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex);
tradeManager.requestPersistence();
} else { } else {
log.warn("Invalid dispute state {}", disputeState.name()); log.warn("Invalid dispute state {}", disputeState.name());
} }
tradeManager.requestPersistence();
} }
private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) { 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(); applyOnDisputeOpened();
ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId()); ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> { ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null) if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED); tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED);
@ -499,7 +499,7 @@ public abstract class TradeStepView extends AnchorPane {
} }
applyOnDisputeOpened(); applyOnDisputeOpened();
ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId()); ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> { ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null) if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
@ -513,7 +513,7 @@ public abstract class TradeStepView extends AnchorPane {
} }
applyOnDisputeOpened(); applyOnDisputeOpened();
ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId()); ownDispute = model.dataModel.mediationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> { ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null) if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED); tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED);
@ -525,7 +525,7 @@ public abstract class TradeStepView extends AnchorPane {
} }
applyOnDisputeOpened(); applyOnDisputeOpened();
ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId()); ownDispute = model.dataModel.mediationManager.findDispute(trade.getId());
ownDispute.ifPresent(dispute -> { ownDispute.ifPresent(dispute -> {
if (tradeStepInfo != null) { if (tradeStepInfo != null) {
tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_PEER_REQUESTED); tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_PEER_REQUESTED);

View file

@ -154,6 +154,82 @@ message RestoreAccountRequest {
message RestoreAccountReply { message RestoreAccountReply {
} }
///////////////////////////////////////////////////////////////////////////////////////////
// Disputes
///////////////////////////////////////////////////////////////////////////////////////////
service Disputes {
rpc GetDispute (GetDisputeRequest) returns (GetDisputeReply) {
}
rpc GetDisputes (GetDisputesRequest) returns (GetDisputesReply) {
}
rpc OpenDispute (OpenDisputeRequest) returns (OpenDisputeReply) {
}
rpc ResolveDispute (ResolveDisputeRequest) returns (ResolveDisputeReply) {
}
rpc SendDisputeChatMessage (SendDisputeChatMessageRequest) returns (SendDisputeChatMessageReply) {
}
}
message GetDisputesRequest {
}
message GetDisputesReply {
repeated Dispute disputes = 1; // pb.proto
}
message GetDisputeRequest {
string trade_id = 1;
}
message GetDisputeReply {
Dispute dispute = 1; // pb.proto
}
message OpenDisputeRequest {
string trade_id = 1;
}
message OpenDisputeReply {
}
message ResolveDisputeReply {
}
message ResolveDisputeRequest {
string trade_id = 1;
DisputeResult.Winner winner = 2;
DisputeResult.Reason reason = 3;
string summary_notes = 4;
uint64 custom_payout_amount = 5 [jstype = JS_STRING];
}
message SendDisputeChatMessageRequest {
string dispute_id = 1;
string message = 2;
repeated Attachment attachments = 3; // pb.proto
}
message SendDisputeChatMessageReply {
}
///////////////////////////////////////////////////////////////////////////////////////////
// DisputeAgents
///////////////////////////////////////////////////////////////////////////////////////////
service DisputeAgents {
rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) {
}
}
message RegisterDisputeAgentRequest {
string dispute_agent_type = 1;
string registration_key = 2;
}
message RegisterDisputeAgentReply {
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Notifications // Notifications
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -306,23 +382,6 @@ message SetAutoSwitchRequest {
message SetAutoSwitchReply {} message SetAutoSwitchReply {}
///////////////////////////////////////////////////////////////////////////////////////////
// DisputeAgents
///////////////////////////////////////////////////////////////////////////////////////////
service DisputeAgents {
rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) {
}
}
message RegisterDisputeAgentRequest {
string dispute_agent_type = 1;
string registration_key = 2;
}
message RegisterDisputeAgentReply {
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Offers // Offers
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////