Don't display multiple disputes from same trade (in case when both open dispute). Delay msg so the order is correct (open dispute msg need to be processed before comm. msg or close msg). Add check for isBoostrapped when opening dispute. Add error msg handler.

This commit is contained in:
Manfred Karrer 2016-07-12 14:55:00 +02:00
parent 9295d23df2
commit 3e18ba94b0
2 changed files with 201 additions and 101 deletions

View File

@ -27,8 +27,12 @@ import io.bitsquare.btc.TradeWalletService;
import io.bitsquare.btc.WalletService;
import io.bitsquare.btc.exceptions.TransactionVerificationException;
import io.bitsquare.btc.exceptions.WalletException;
import io.bitsquare.common.Timer;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.common.crypto.PubKeyRing;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.crypto.DecryptedMsgWithPubKey;
import io.bitsquare.p2p.BootstrapListener;
import io.bitsquare.p2p.Message;
@ -53,9 +57,12 @@ import org.slf4j.LoggerFactory;
import javax.inject.Named;
import java.io.File;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DisputeManager {
private static final Logger log = LoggerFactory.getLogger(DisputeManager.class);
@ -75,6 +82,7 @@ public class DisputeManager {
private final CopyOnWriteArraySet<DecryptedMsgWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
private final Map<String, Dispute> openDisputes;
private final Map<String, Dispute> closedDisputes;
private Map<String, Timer> delayMsgMap = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
@ -197,43 +205,55 @@ public class DisputeManager {
log.warn("Unsupported message at dispatchMessage.\nmessage=" + message);
}
public void sendOpenNewDisputeMessage(Dispute dispute) {
public void sendOpenNewDisputeMessage(Dispute dispute, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
if (!disputes.contains(dispute)) {
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(dispute.getTradeId(),
keyRing.getPubKeyRing().hashCode(),
true,
"System message: " + (dispute.isSupportTicket() ?
"You opened a request for support."
: "You opened a request for a dispute.\n\n" + disputeInfo),
p2PService.getAddress());
disputeCommunicationMessage.setIsSystemMessage(true);
dispute.addDisputeMessage(disputeCommunicationMessage);
disputes.add(dispute);
disputesObservableList.add(dispute);
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
if (!storedDisputeOptional.isPresent()) {
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(dispute.getTradeId(),
keyRing.getPubKeyRing().hashCode(),
true,
"System message: " + (dispute.isSupportTicket() ?
"You opened a request for support."
: "You opened a request for a dispute.\n\n" + disputeInfo),
p2PService.getAddress());
disputeCommunicationMessage.setIsSystemMessage(true);
dispute.addDisputeMessage(disputeCommunicationMessage);
disputes.add(dispute);
disputesObservableList.add(dispute);
p2PService.sendEncryptedMailboxMessage(dispute.getContract().arbitratorNodeAddress,
dispute.getArbitratorPubKeyRing(),
new OpenNewDisputeMessage(dispute, p2PService.getAddress()),
new SendMailboxMessageListener() {
@Override
public void onArrived() {
disputeCommunicationMessage.setArrived(true);
p2PService.sendEncryptedMailboxMessage(dispute.getContract().arbitratorNodeAddress,
dispute.getArbitratorPubKeyRing(),
new OpenNewDisputeMessage(dispute, p2PService.getAddress()),
new SendMailboxMessageListener() {
@Override
public void onArrived() {
disputeCommunicationMessage.setArrived(true);
resultHandler.handleResult();
}
@Override
public void onStoredInMailbox() {
disputeCommunicationMessage.setStoredInMailbox(true);
resultHandler.handleResult();
}
@Override
public void onFault(String errorMessage) {
log.error("sendEncryptedMessage failed");
errorMessageHandler.handleErrorMessage("Sending dispute message failed: " + errorMessage);
}
}
@Override
public void onStoredInMailbox() {
disputeCommunicationMessage.setStoredInMailbox(true);
}
@Override
public void onFault(String errorMessage) {
log.error("sendEncryptedMessage failed");
}
}
);
);
} else {
final String msg = "We got a dispute already open for that trade and trading peer.\n" +
"TradeId = " + dispute.getTradeId();
log.warn(msg);
errorMessageHandler.handleErrorMessage(msg);
}
} else {
log.warn("We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId());
final String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
log.warn(msg);
errorMessageHandler.handleErrorMessage(msg);
}
}
@ -261,43 +281,49 @@ public class DisputeManager {
disputeFromOpener.getArbitratorPubKeyRing(),
disputeFromOpener.isSupportTicket()
);
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(dispute.getTradeId(),
keyRing.getPubKeyRing().hashCode(),
true,
"System message: " + (dispute.isSupportTicket() ?
"Your trading peer has requested support due technical problems. Please wait for further instructions."
: "Your trading peer has requested a dispute.\n\n" + disputeInfo),
p2PService.getAddress());
disputeCommunicationMessage.setIsSystemMessage(true);
dispute.addDisputeMessage(disputeCommunicationMessage);
disputes.add(dispute);
disputesObservableList.add(dispute);
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
if (!storedDisputeOptional.isPresent()) {
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(dispute.getTradeId(),
keyRing.getPubKeyRing().hashCode(),
true,
"System message: " + (dispute.isSupportTicket() ?
"Your trading peer has requested support due technical problems. Please wait for further instructions."
: "Your trading peer has requested a dispute.\n\n" + disputeInfo),
p2PService.getAddress());
disputeCommunicationMessage.setIsSystemMessage(true);
dispute.addDisputeMessage(disputeCommunicationMessage);
disputes.add(dispute);
disputesObservableList.add(dispute);
// we mirrored dispute already!
Contract contract = dispute.getContract();
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
NodeAddress peerNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress();
log.trace("sendPeerOpenedDisputeMessage to peerAddress " + peerNodeAddress);
p2PService.sendEncryptedMailboxMessage(peerNodeAddress,
peersPubKeyRing,
new PeerOpenedDisputeMessage(dispute, p2PService.getAddress()),
new SendMailboxMessageListener() {
@Override
public void onArrived() {
disputeCommunicationMessage.setArrived(true);
}
// we mirrored dispute already!
Contract contract = dispute.getContract();
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
NodeAddress peerNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress();
log.trace("sendPeerOpenedDisputeMessage to peerAddress " + peerNodeAddress);
p2PService.sendEncryptedMailboxMessage(peerNodeAddress,
peersPubKeyRing,
new PeerOpenedDisputeMessage(dispute, p2PService.getAddress()),
new SendMailboxMessageListener() {
@Override
public void onArrived() {
disputeCommunicationMessage.setArrived(true);
}
@Override
public void onStoredInMailbox() {
disputeCommunicationMessage.setStoredInMailbox(true);
}
@Override
public void onStoredInMailbox() {
disputeCommunicationMessage.setStoredInMailbox(true);
}
@Override
public void onFault(String errorMessage) {
log.error("sendEncryptedMessage failed");
@Override
public void onFault(String errorMessage) {
log.error("sendEncryptedMessage failed");
}
}
}
);
);
} else {
log.warn("We got a dispute already open for that trade and trading peer.\n" +
"TradeId = " + dispute.getTradeId());
}
}
// traders send msg to the arbitrator or arbitrator to 1 trader (trader to trader is not allowed)
@ -428,10 +454,16 @@ public class DisputeManager {
Dispute dispute = openNewDisputeMessage.dispute;
if (isArbitrator(dispute)) {
if (!disputes.contains(dispute)) {
dispute.setStorage(getDisputeStorage());
disputes.add(dispute);
disputesObservableList.add(dispute);
sendPeerOpenedDisputeMessage(dispute);
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
if (!storedDisputeOptional.isPresent()) {
dispute.setStorage(getDisputeStorage());
disputes.add(dispute);
disputesObservableList.add(dispute);
sendPeerOpenedDisputeMessage(dispute);
} else {
log.warn("We got a dispute already open for that trade and trading peer.\n" +
"TradeId = " + dispute.getTradeId());
}
} else {
log.warn("We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId());
}
@ -444,14 +476,20 @@ public class DisputeManager {
private void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) {
Dispute dispute = peerOpenedDisputeMessage.dispute;
if (!isArbitrator(dispute)) {
Optional<Trade> tradeOptional = tradeManager.getTradeById(dispute.getTradeId());
if (tradeOptional.isPresent())
tradeOptional.get().setDisputeState(Trade.DisputeState.DISPUTE_STARTED_BY_PEER);
if (!disputes.contains(dispute)) {
dispute.setStorage(getDisputeStorage());
disputes.add(dispute);
disputesObservableList.add(dispute);
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
if (!storedDisputeOptional.isPresent()) {
Optional<Trade> tradeOptional = tradeManager.getTradeById(dispute.getTradeId());
if (tradeOptional.isPresent())
tradeOptional.get().setDisputeState(Trade.DisputeState.DISPUTE_STARTED_BY_PEER);
dispute.setStorage(getDisputeStorage());
disputes.add(dispute);
disputesObservableList.add(dispute);
} else {
log.warn("We got a dispute already open for that trade and trading peer.\n" +
"TradeId = " + dispute.getTradeId());
}
} else {
log.warn("We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId());
}
@ -462,16 +500,26 @@ public class DisputeManager {
// a trader can receive a msg from the arbitrator or the arbitrator form a trader. Trader to trader is not allowed.
private void onDisputeDirectMessage(DisputeCommunicationMessage disputeCommunicationMessage) {
Log.traceCall("disputeDirectMessage " + disputeCommunicationMessage);
Optional<Dispute> disputeOptional = findDispute(disputeCommunicationMessage.getTradeId(), disputeCommunicationMessage.getTraderId());
Log.traceCall("disputeCommunicationMessage " + disputeCommunicationMessage);
final String tradeId = disputeCommunicationMessage.getTradeId();
Optional<Dispute> disputeOptional = findDispute(tradeId, disputeCommunicationMessage.getTraderId());
final String uid = disputeCommunicationMessage.getUID();
if (disputeOptional.isPresent()) {
cleanupRetryMap(uid);
Dispute dispute = disputeOptional.get();
if (!dispute.getDisputeCommunicationMessagesAsObservableList().contains(disputeCommunicationMessage))
dispute.addDisputeMessage(disputeCommunicationMessage);
else
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + disputeCommunicationMessage.getTradeId());
log.warn("We got a disputeCommunicationMessage what we have already stored. TradeId = " + tradeId);
} else {
log.warn("We got a dispute mail msg but we don't have a matching dispute. TradeId = " + disputeCommunicationMessage.getTradeId());
log.debug("We got a disputeCommunicationMessage but we don't have a matching dispute. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
Timer timer = UserThread.runAfter(() -> onDisputeDirectMessage(disputeCommunicationMessage), 1);
delayMsgMap.put(uid, timer);
} else {
log.warn("We got a disputeCommunicationMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId);
}
}
}
@ -479,8 +527,12 @@ public class DisputeManager {
private void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) {
DisputeResult disputeResult = disputeResultMessage.disputeResult;
if (!isArbitrator(disputeResult)) {
Optional<Dispute> disputeOptional = findDispute(disputeResult.tradeId, disputeResult.traderId);
final String tradeId = disputeResult.tradeId;
Optional<Dispute> disputeOptional = findDispute(tradeId, disputeResult.traderId);
final String uid = disputeResultMessage.getUID();
if (disputeOptional.isPresent()) {
cleanupRetryMap(uid);
Dispute dispute = disputeOptional.get();
DisputeCommunicationMessage disputeCommunicationMessage = disputeResult.getDisputeCommunicationMessage();
@ -506,12 +558,12 @@ public class DisputeManager {
|| (isBuyer && disputeResult.getWinner() == DisputeResult.Winner.STALE_MATE)) {
final Optional<Trade> tradeOptional = tradeManager.getTradeById(disputeResult.tradeId);
final Optional<Trade> tradeOptional = tradeManager.getTradeById(tradeId);
Transaction payoutTx = null;
if (tradeOptional.isPresent()) {
payoutTx = tradeOptional.get().getPayoutTx();
} else {
final Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(disputeResult.tradeId);
final Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx();
}
@ -560,7 +612,6 @@ public class DisputeManager {
@Override
public void onFailure(@NotNull Throwable t) {
// TODO error handling
log.error(t.getMessage());
}
});
@ -569,19 +620,30 @@ public class DisputeManager {
log.error("Error at traderSignAndFinalizeDisputedPayoutTx " + e.getMessage());
}
} else {
log.warn("DepositTx is null. TradeId = " + disputeResult.tradeId);
log.warn("DepositTx is null. TradeId = " + tradeId);
}
} else {
log.warn("We got already a payout tx. That might be the case if the other peer did not get the payout tx and opened a dispute. TradeId = " + disputeResult.tradeId);
log.warn("We got already a payout tx. That might be the case if the other peer did not get the " +
"payout tx and opened a dispute. TradeId = " + tradeId);
dispute.setDisputePayoutTxId(payoutTx.getHashAsString());
sendPeerPublishedPayoutTxMessage(payoutTx, dispute, contract);
}
}
} else {
log.warn("We got a dispute msg what we have already stored. TradeId = " + disputeResult.tradeId);
log.warn("We got a dispute msg what we have already stored. TradeId = " + tradeId);
}
} else {
log.warn("We got a dispute result msg but we don't have a matching dispute. TradeId = " + disputeResult.tradeId);
log.debug("We got a dispute result msg but we don't have a matching dispute. " +
"That might happen when we get the disputeResultMessage before the dispute was created. " +
"We try again after 1 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
// We delay2 sec. to be sure the comm. msg gets added first
Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2);
delayMsgMap.put(uid, timer);
} else {
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
"That should never happen. TradeId = " + tradeId);
}
}
} else {
log.error("Arbitrator received disputeResultMessage. That must never happen.");
@ -590,9 +652,26 @@ public class DisputeManager {
// losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer
private void onDisputedPayoutTxMessage(PeerPublishedPayoutTxMessage peerPublishedPayoutTxMessage) {
Transaction transaction = tradeWalletService.addTransactionToWallet(peerPublishedPayoutTxMessage.transaction);
findOwnDispute(peerPublishedPayoutTxMessage.tradeId).ifPresent(dispute -> dispute.setDisputePayoutTxId(transaction.getHashAsString()));
tradeManager.closeDisputedTrade(peerPublishedPayoutTxMessage.tradeId);
final String uid = peerPublishedPayoutTxMessage.getUID();
final String tradeId = peerPublishedPayoutTxMessage.tradeId;
Optional<Dispute> disputeOptional = findOwnDispute(tradeId);
if (disputeOptional.isPresent()) {
cleanupRetryMap(uid);
Transaction transaction = tradeWalletService.addTransactionToWallet(peerPublishedPayoutTxMessage.transaction);
disputeOptional.get().setDisputePayoutTxId(transaction.getHashAsString());
tradeManager.closeDisputedTrade(tradeId);
} else {
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
// We delay 3 sec. to be sure the close msg gets added first
Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedPayoutTxMessage), 3);
delayMsgMap.put(uid, timer);
} else {
log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " +
"That should never happen. TradeId = " + tradeId);
}
}
}
@ -630,11 +709,19 @@ public class DisputeManager {
}
public Optional<Dispute> findOwnDispute(String tradeId) {
return disputes.stream().filter(e -> e.getTradeId().equals(tradeId)).findAny();
return getDisputeStream(tradeId).findAny();
}
public List<Dispute> findDisputesByTradeId(String tradeId) {
return disputes.stream().filter(e -> e.getTradeId().equals(tradeId)).collect(Collectors.toList());
private Stream<Dispute> getDisputeStream(String tradeId) {
return disputes.stream().filter(e -> e.getTradeId().equals(tradeId));
}
private void cleanupRetryMap(String uid) {
if (delayMsgMap.containsKey(uid)) {
Timer timer = delayMsgMap.remove(uid);
if (timer != null)
timer.stop();
}
}
}

View File

@ -19,6 +19,7 @@ package io.bitsquare.gui.main.portfolio.pendingtrades;
import com.google.inject.Inject;
import io.bitsquare.app.Log;
import io.bitsquare.arbitration.Arbitrator;
import io.bitsquare.arbitration.Dispute;
import io.bitsquare.arbitration.DisputeManager;
import io.bitsquare.btc.FeePolicy;
@ -33,8 +34,10 @@ import io.bitsquare.gui.common.model.ActivatableDataModel;
import io.bitsquare.gui.main.MainView;
import io.bitsquare.gui.main.disputes.DisputesView;
import io.bitsquare.gui.main.overlays.notifications.NotificationCenter;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.overlays.windows.SelectDepositTxWindow;
import io.bitsquare.gui.main.overlays.windows.WalletPasswordWindow;
import io.bitsquare.p2p.P2PService;
import io.bitsquare.payment.PaymentAccountContractData;
import io.bitsquare.trade.BuyerTrade;
import io.bitsquare.trade.SellerTrade;
@ -70,6 +73,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
private final User user;
private final KeyRing keyRing;
public final DisputeManager disputeManager;
private P2PService p2PService;
public final Navigation navigation;
public final WalletPasswordWindow walletPasswordWindow;
private final NotificationCenter notificationCenter;
@ -90,7 +94,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
@Inject
public PendingTradesDataModel(TradeManager tradeManager, WalletService walletService, TradeWalletService tradeWalletService,
User user, KeyRing keyRing, DisputeManager disputeManager, Preferences preferences,
User user, KeyRing keyRing, DisputeManager disputeManager, Preferences preferences, P2PService p2PService,
Navigation navigation, WalletPasswordWindow walletPasswordWindow, NotificationCenter notificationCenter) {
this.tradeManager = tradeManager;
this.walletService = walletService;
@ -99,6 +103,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
this.keyRing = keyRing;
this.disputeManager = disputeManager;
this.preferences = preferences;
this.p2PService = p2PService;
this.navigation = navigation;
this.walletPasswordWindow = walletPasswordWindow;
this.notificationCenter = notificationCenter;
@ -352,6 +357,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
log.debug("payoutTx is null at doOpenDispute");
}
final Arbitrator acceptedArbitratorByAddress = user.getAcceptedArbitratorByAddress(trade.getArbitratorNodeAddress());
checkNotNull(acceptedArbitratorByAddress);
Dispute dispute = new Dispute(disputeManager.getDisputeStorage(),
trade.getId(),
keyRing.getPubKeyRing().hashCode(), // traderId
@ -368,13 +375,19 @@ public class PendingTradesDataModel extends ActivatableDataModel {
trade.getContractAsJson(),
trade.getOffererContractSignature(),
trade.getTakerContractSignature(),
user.getAcceptedArbitratorByAddress(trade.getArbitratorNodeAddress()).getPubKeyRing(),
acceptedArbitratorByAddress.getPubKeyRing(),
isSupportTicket
);
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
disputeManager.sendOpenNewDisputeMessage(dispute);
navigation.navigateTo(MainView.class, DisputesView.class);
if (p2PService.isBootstrapped()) {
disputeManager.sendOpenNewDisputeMessage(dispute,
() -> navigation.navigateTo(MainView.class, DisputesView.class),
errorMessage -> new Popup().warning(errorMessage).show());
} else {
new Popup().information("You need to wait until you are fully connected to the network.\n" +
"That might take up to about 2 minutes at startup.").show();
}
} else {
log.warn("trade is null at doOpenDispute");
}