mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 19:49:32 +00:00
Add API functions to open and resolve disputes (#244)
Co-authored-by: woodser <woodser@protonmail.com>
This commit is contained in:
parent
07c48a04f5
commit
e7b4627102
22 changed files with 752 additions and 306 deletions
|
@ -29,6 +29,9 @@ import bisq.core.offer.OfferPayload;
|
|||
import bisq.core.offer.OpenOffer;
|
||||
import bisq.core.payment.PaymentAccount;
|
||||
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.statistics.TradeStatistics3;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
|
@ -36,6 +39,7 @@ import bisq.common.app.Version;
|
|||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.IncorrectPasswordException;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.FaultHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
||||
import bisq.proto.grpc.NotificationMessage;
|
||||
|
@ -79,6 +83,7 @@ public class CoreApi {
|
|||
private final AppStartupState appStartupState;
|
||||
private final CoreAccountService coreAccountService;
|
||||
private final CoreDisputeAgentsService coreDisputeAgentsService;
|
||||
private final CoreDisputesService coreDisputeService;
|
||||
private final CoreHelpService coreHelpService;
|
||||
private final CoreOffersService coreOffersService;
|
||||
private final CorePaymentAccountsService paymentAccountsService;
|
||||
|
@ -94,6 +99,7 @@ public class CoreApi {
|
|||
AppStartupState appStartupState,
|
||||
CoreAccountService coreAccountService,
|
||||
CoreDisputeAgentsService coreDisputeAgentsService,
|
||||
CoreDisputesService coreDisputeService,
|
||||
CoreHelpService coreHelpService,
|
||||
CoreOffersService coreOffersService,
|
||||
CorePaymentAccountsService paymentAccountsService,
|
||||
|
@ -107,6 +113,7 @@ public class CoreApi {
|
|||
this.appStartupState = appStartupState;
|
||||
this.coreAccountService = coreAccountService;
|
||||
this.coreDisputeAgentsService = coreDisputeAgentsService;
|
||||
this.coreDisputeService = coreDisputeService;
|
||||
this.coreHelpService = coreHelpService;
|
||||
this.coreOffersService = coreOffersService;
|
||||
this.paymentAccountsService = paymentAccountsService;
|
||||
|
@ -333,6 +340,31 @@ public class CoreApi {
|
|||
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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
335
core/src/main/java/bisq/core/api/CoreDisputesService.java
Normal file
335
core/src/main/java/bisq/core/api/CoreDisputesService.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package bisq.core.api;
|
|||
import bisq.core.api.CoreApi.NotificationListener;
|
||||
import bisq.core.api.model.TradeInfo;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.proto.grpc.NotificationMessage;
|
||||
import bisq.proto.grpc.NotificationMessage.NotificationType;
|
||||
import javax.inject.Singleton;
|
||||
|
@ -56,4 +57,12 @@ public class CoreNotificationService {
|
|||
.setTitle(title)
|
||||
.setMessage(message).build());
|
||||
}
|
||||
|
||||
public void sendChatNotification(ChatMessage chatMessage) {
|
||||
sendNotification(NotificationMessage.newBuilder()
|
||||
.setType(NotificationType.CHAT_MESSAGE)
|
||||
.setTimestamp(System.currentTimeMillis())
|
||||
.setChatMessage(chatMessage.toProtoChatMessageBuilder())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package bisq.core.support;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.api.CoreNotificationService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.support.messages.SupportMessage;
|
||||
|
@ -48,6 +49,7 @@ import javax.annotation.Nullable;
|
|||
public abstract class SupportManager {
|
||||
protected final P2PService p2PService;
|
||||
protected final CoreMoneroConnectionsService connectionService;
|
||||
protected final CoreNotificationService notificationService;
|
||||
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||
|
@ -59,10 +61,11 @@ public abstract class SupportManager {
|
|||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService) {
|
||||
public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService, CoreNotificationService notificationService) {
|
||||
this.p2PService = p2PService;
|
||||
this.connectionService = connectionService;
|
||||
mailboxMessageService = p2PService.getMailboxMessageService();
|
||||
this.mailboxMessageService = p2PService.getMailboxMessageService();
|
||||
this.notificationService = notificationService;
|
||||
|
||||
// We get first the message handler called then the onBootstrapped
|
||||
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||
|
@ -152,6 +155,7 @@ public abstract class SupportManager {
|
|||
PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage);
|
||||
|
||||
addAndPersistChatMessage(chatMessage);
|
||||
notificationService.sendChatNotification(chatMessage);
|
||||
|
||||
// We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we
|
||||
// cannot send it anyway)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.api.CoreNotificationService;
|
||||
import bisq.core.btc.wallet.Restrictions;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
|
@ -111,6 +112,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
TradeWalletService tradeWalletService,
|
||||
XmrWalletService xmrWalletService,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
CoreNotificationService notificationService,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
|
@ -118,7 +120,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
DisputeListService<T> disputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, connectionService);
|
||||
super(p2PService, connectionService, notificationService);
|
||||
|
||||
this.tradeWalletService = tradeWalletService;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
|
@ -288,17 +290,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -823,6 +814,17 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
.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) {
|
||||
Optional<Trade> retVal = tradeManager.getTradeById(dispute.getTradeId());
|
||||
if (!retVal.isPresent()) {
|
||||
|
|
|
@ -15,13 +15,9 @@
|
|||
* 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.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;
|
|
@ -18,6 +18,7 @@
|
|||
package bisq.core.support.dispute.arbitration;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.api.CoreNotificationService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
|
@ -60,8 +61,6 @@ import bisq.common.crypto.PubKeyRing;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -96,6 +95,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
TradeWalletService tradeWalletService,
|
||||
XmrWalletService walletService,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
CoreNotificationService notificationService,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
|
@ -103,7 +103,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
ArbitrationDisputeListService arbitrationDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
|
||||
openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService);
|
||||
}
|
||||
|
||||
|
@ -286,7 +286,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
MoneroTxSet txSet = traderSignsDisputePayoutTx(tradeId, arbitratorSignedPayoutTxHex);
|
||||
onTraderSignedDisputePayoutTx(tradeId, txSet);
|
||||
} 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);
|
||||
success = false;
|
||||
}
|
||||
|
@ -318,7 +319,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
// }
|
||||
// catch (AddressFormatException | WalletException e) {
|
||||
catch (Exception e) {
|
||||
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
|
||||
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx: " + e.toString();
|
||||
log.error(errorMessage, e);
|
||||
success = false;
|
||||
|
||||
|
@ -343,7 +344,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
|
||||
String uid = peerPublishedDisputePayoutTxMessage.getUid();
|
||||
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
|
||||
Optional<Dispute> disputeOptional = findOwnDispute(tradeId);
|
||||
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
|
||||
if (!delayMsgMap.containsKey(uid)) {
|
||||
|
@ -473,7 +474,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
|
||||
// gather trade info
|
||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
||||
Optional<Dispute> disputeOptional = findOwnDispute(tradeId);
|
||||
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
||||
return;
|
||||
|
@ -579,78 +580,46 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
// Disputed payout tx signing
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO (woodser): where to move this common logic?
|
||||
public static MoneroTxWallet arbitratorCreatesDisputedPayoutTx(Contract contract, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) {
|
||||
|
||||
//System.out.println("DisputeSummaryWindow.arbitratorSignsDisputedPayoutTx()");
|
||||
//System.out.println("=== DISPUTE ===");
|
||||
//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);
|
||||
// multisig wallet must be synced
|
||||
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");
|
||||
|
||||
// gather relevant trade info
|
||||
String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString();
|
||||
String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
|
||||
Preconditions.checkNotNull(buyerPayoutAddress, "buyerPayoutAddress must not be null");
|
||||
Preconditions.checkNotNull(sellerPayoutAddress, "sellerPayoutAddress must not be null");
|
||||
BigInteger buyerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getBuyerPayoutAmount());
|
||||
BigInteger sellerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getSellerPayoutAmount());
|
||||
|
||||
//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)));
|
||||
// collect winner and loser payout address and amounts
|
||||
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
|
||||
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
|
||||
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
|
||||
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
|
||||
BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
||||
BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
|
||||
|
||||
// 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
|
||||
//System.out.println("Creating feeEstimateTx!");
|
||||
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 (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5)));
|
||||
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 (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
|
||||
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig);
|
||||
|
||||
System.out.println("Created fee estimate tx!");
|
||||
System.out.println(feeEstimateTx);
|
||||
//BigInteger estimatedFee = feeEstimateTx.getFee();
|
||||
|
||||
// attempt to create payout tx by increasing estimated fee until successful
|
||||
// create payout tx by increasing estimated fee until successful
|
||||
MoneroTxWallet payoutTx = null;
|
||||
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) {
|
||||
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);
|
||||
if (buyerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(feeDivisor)))); // split fee subtracted from each payout amount
|
||||
if (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(feeDivisor))));
|
||||
try {
|
||||
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 (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
|
||||
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++;
|
||||
try {
|
||||
payoutTx = multisigWallet.createTx(txConfig);
|
||||
} catch (MoneroError e) {
|
||||
// exception expected // TODO: better way of estimating fee?
|
||||
}
|
||||
}
|
||||
|
||||
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx");
|
||||
System.out.println("DISPUTE PAYOUT TX GENERATED ON ATTEMPT " + numAttempts);
|
||||
System.out.println(payoutTx);
|
||||
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
|
||||
log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
|
||||
return payoutTx;
|
||||
}
|
||||
|
||||
|
@ -658,7 +627,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
|
||||
// gather trade info
|
||||
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);
|
||||
Dispute dispute = disputeOptional.get();
|
||||
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 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 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
|
||||
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");
|
||||
|
||||
// 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
|
||||
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");
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
System.out.println("Updating multisig hex from arbitrator: " + disputeResult.getArbitratorUpdatedMultisigHex());
|
||||
System.out.println("Updating multisig hex from arbitrator");
|
||||
multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex()));
|
||||
|
||||
// sign arbitrator-signed payout tx
|
||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
||||
|
||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package bisq.core.support.dispute.mediation;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.api.CoreNotificationService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
|
@ -78,6 +79,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||
TradeWalletService tradeWalletService,
|
||||
XmrWalletService walletService,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
CoreNotificationService notificationService,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
|
@ -85,7 +87,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||
MediationDisputeListService mediationDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
|
||||
openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package bisq.core.support.dispute.refund;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.api.CoreNotificationService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
|
@ -72,6 +73,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
TradeWalletService tradeWalletService,
|
||||
XmrWalletService walletService,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
CoreNotificationService notificationService,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
|
@ -80,7 +82,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
RefundDisputeListService refundDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
|
||||
openOfferManager, keyRing, refundDisputeListService, config, priceFeedService);
|
||||
}
|
||||
|
||||
|
|
|
@ -205,9 +205,7 @@ public final class ChatMessage extends SupportMessage {
|
|||
notifyChangeListener();
|
||||
}
|
||||
|
||||
// We cannot rename protobuf definition because it would break backward compatibility
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
public protobuf.ChatMessage.Builder toProtoChatMessageBuilder() {
|
||||
protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder()
|
||||
.setType(SupportType.toProtoMessage(supportType))
|
||||
.setTradeId(tradeId)
|
||||
|
@ -225,6 +223,14 @@ public final class ChatMessage extends SupportMessage {
|
|||
.setWasDisplayed(wasDisplayed);
|
||||
Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError);
|
||||
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()
|
||||
.setChatMessage(builder)
|
||||
.build();
|
||||
|
|
|
@ -21,8 +21,6 @@ import bisq.core.support.SupportSession;
|
|||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.trade.Trade;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package bisq.core.support.traderchat;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.api.CoreNotificationService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.SupportManager;
|
||||
import bisq.core.support.SupportType;
|
||||
|
@ -57,9 +58,10 @@ public class TraderChatManager extends SupportManager {
|
|||
@Inject
|
||||
public TraderChatManager(P2PService p2PService,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
CoreNotificationService notificationService,
|
||||
TradeManager tradeManager,
|
||||
PubKeyRingProvider pubKeyRingProvider) {
|
||||
super(p2PService, connectionService);
|
||||
super(p2PService, connectionService, notificationService);
|
||||
this.tradeManager = tradeManager;
|
||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ public class ParsingUtils {
|
|||
* Multiplier to convert centineros (the base XMR unit of Coin) to atomic units.
|
||||
*
|
||||
* TODO: change base unit to atomic units and long
|
||||
* TODO: move these static utilities?
|
||||
*/
|
||||
private static BigInteger CENTINEROS_AU_MULTIPLIER = BigInteger.valueOf(10000);
|
||||
|
||||
|
|
156
daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java
Normal file
156
daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java
Normal 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));
|
||||
}}
|
||||
)));
|
||||
}
|
||||
}
|
|
@ -51,6 +51,7 @@ public class GrpcServer {
|
|||
PasswordAuthInterceptor passwordAuthInterceptor,
|
||||
GrpcAccountService accountService,
|
||||
GrpcDisputeAgentsService disputeAgentsService,
|
||||
GrpcDisputesService disputesService,
|
||||
GrpcHelpService helpService,
|
||||
GrpcOffersService offersService,
|
||||
GrpcPaymentAccountsService paymentAccountsService,
|
||||
|
@ -66,6 +67,7 @@ public class GrpcServer {
|
|||
.executor(UserThread.getExecutor())
|
||||
.addService(interceptForward(accountService, accountService.interceptors()))
|
||||
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
|
||||
.addService(interceptForward(disputesService, disputesService.interceptors()))
|
||||
.addService(interceptForward(helpService, helpService.interceptors()))
|
||||
.addService(interceptForward(offersService, offersService.interceptors()))
|
||||
.addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors()))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -154,6 +154,82 @@ message RestoreAccountRequest {
|
|||
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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -306,23 +382,6 @@ message SetAutoSwitchRequest {
|
|||
|
||||
message SetAutoSwitchReply {}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// DisputeAgents
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
service DisputeAgents {
|
||||
rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) {
|
||||
}
|
||||
}
|
||||
|
||||
message RegisterDisputeAgentRequest {
|
||||
string dispute_agent_type = 1;
|
||||
string registration_key = 2;
|
||||
}
|
||||
|
||||
message RegisterDisputeAgentReply {
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Offers
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
Loading…
Reference in a new issue