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.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;
@ -134,7 +141,7 @@ public class CoreApi {
///////////////////////////////////////////////////////////////////////////////////////////
// Account Service
///////////////////////////////////////////////////////////////////////////////////////////
public boolean accountExists() {
return coreAccountService.accountExists();
}
@ -174,7 +181,7 @@ public class CoreApi {
public void restoreAccount(InputStream zipStream, int bufferSize, Runnable onShutdown) throws Exception {
coreAccountService.restoreAccount(zipStream, bufferSize, onShutdown);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Monero Connections
///////////////////////////////////////////////////////////////////////////////////////////
@ -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
///////////////////////////////////////////////////////////////////////////////////////////

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.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());
}
}

View file

@ -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)

View file

@ -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()) {

View file

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

View file

@ -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,86 +580,54 @@ 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());
// 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());
//System.out.println("buyerPayoutAddress: " + buyerPayoutAddress);
//System.out.println("buyerPayoutAmount: " + buyerPayoutAmount);
// create transaction to get fee estimate
// TODO (woodser): include arbitration fee
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
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);
// 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
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)));
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
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
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 {
// create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null;
int numAttempts = 0;
while (payoutTx == null && numAttempts < 50) {
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 (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++;
payoutTx = multisigWallet.createTx(txConfig);
} catch (MoneroError e) {
// exception expected // TODO: better way of estimating fee?
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);
return 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;
}
private MoneroTxSet traderSignsDisputePayoutTx(String tradeId, String payoutTxHex) {
// 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);

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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();

View file

@ -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;

View file

@ -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;
}

View file

@ -21,12 +21,13 @@ 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);
/**
* Convert Coin (denominated in centineros) to atomic units.
*
*
* @param coin has an amount denominated in centineros
* @return BigInteger the coin amount denominated in atomic units
*/
@ -43,7 +44,7 @@ public class ParsingUtils {
public static BigInteger centinerosToAtomicUnits(long centineros) {
return BigInteger.valueOf(centineros).multiply(ParsingUtils.CENTINEROS_AU_MULTIPLIER);
}
/**
* Convert atomic units to centineros.
*