/*
* This file is part of Bitsquare.
*
* Bitsquare 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.
*
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.arbitration;
import com.google.common.util.concurrent.FutureCallback;
import com.google.inject.Inject;
import io.bitsquare.app.Log;
import io.bitsquare.arbitration.messages.*;
import io.bitsquare.arbitration.payload.Attachment;
import io.bitsquare.btc.AddressEntry;
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.FaultHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.crypto.DecryptedMsgWithPubKey;
import io.bitsquare.p2p.BootstrapListener;
import io.bitsquare.p2p.Message;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.p2p.P2PService;
import io.bitsquare.p2p.messaging.SendMailboxMessageListener;
import io.bitsquare.storage.Storage;
import io.bitsquare.trade.Contract;
import io.bitsquare.trade.Tradable;
import io.bitsquare.trade.Trade;
import io.bitsquare.trade.TradeManager;
import io.bitsquare.trade.closed.ClosedTradableManager;
import io.bitsquare.trade.offer.OpenOffer;
import io.bitsquare.trade.offer.OpenOfferManager;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Transaction;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import java.io.File;
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);
private final TradeWalletService tradeWalletService;
private final WalletService walletService;
private final TradeManager tradeManager;
private ClosedTradableManager closedTradableManager;
private final OpenOfferManager openOfferManager;
private final P2PService p2PService;
private final KeyRing keyRing;
private final Storage<DisputeList<Dispute>> disputeStorage;
private final DisputeList<Dispute> disputes;
transient private final ObservableList<Dispute> disputesObservableList;
private final String disputeInfo;
private final CopyOnWriteArraySet<DecryptedMsgWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
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<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public DisputeManager(P2PService p2PService,
TradeWalletService tradeWalletService,
WalletService walletService,
TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager,
KeyRing keyRing,
@Named(Storage.DIR_KEY) File storageDir) {
this.p2PService = p2PService;
this.tradeWalletService = tradeWalletService;
this.walletService = walletService;
this.tradeManager = tradeManager;
this.closedTradableManager = closedTradableManager;
this.openOfferManager = openOfferManager;
this.keyRing = keyRing;
disputeStorage = new Storage<>(storageDir);
disputes = new DisputeList<>(disputeStorage);
disputesObservableList = FXCollections.observableArrayList(disputes);
openDisputes = new HashMap<>();
closedDisputes = new HashMap<>();
disputes.stream().forEach(dispute -> dispute.setStorage(getDisputeStorage()));
disputeInfo = "Please note the basic rules for the dispute process:\n" +
"1. You need to respond to the arbitrators requests in between 2 days.\n" +
"2. The maximum period for the dispute is 14 days.\n" +
"3. You need to fulfill what the arbitrator is requesting from you to deliver evidence for your case.\n" +
"4. You accepted the rules outlined in the wiki in the user agreement when you first started the application.\n\n" +
"Please read more in detail about the dispute process in our wiki:\nhttps://github" +
".com/bitsquare/bitsquare/wiki/Dispute-process";
// We get first the message handler called then the onBootstrapped
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
if (p2PService.isBootstrapped())
applyMessages();
});
p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey);
if (p2PService.isBootstrapped())
applyMessages();
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() {
if (p2PService.isBootstrapped())
applyMessages();
else
p2PService.addP2PServiceListener(new BootstrapListener() {
@Override
public void onBootstrapComplete() {
applyMessages();
}
});
cleanupDisputes();
}
public void cleanupDisputes() {
disputes.stream().forEach(dispute -> {
dispute.setStorage(getDisputeStorage());
if (dispute.isClosed())
closedDisputes.put(dispute.getTradeId(), dispute);
else
openDisputes.put(dispute.getTradeId(), dispute);
});
// If we have duplicate disputes we close the second one (might happen if both traders opened a dispute and arbitrator
// was offline, so could not forward msg to other peer, then the arbitrator might have 4 disputes open for 1 trade)
openDisputes.entrySet().stream().forEach(openDisputeEntry -> {
String key = openDisputeEntry.getKey();
if (closedDisputes.containsKey(key)) {
final Dispute closedDispute = closedDisputes.get(key);
final Dispute openDispute = openDisputeEntry.getValue();
// We need to check if is from the same peer, we don't want to close the peers dispute
if (closedDispute.getTraderId() == openDispute.getTraderId()) {
openDispute.setIsClosed(true);
tradeManager.closeDisputedTrade(openDispute.getTradeId());
}
}
});
}
private void applyMessages() {
decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
Message message = decryptedMessageWithPubKey.message;
log.debug("decryptedDirectMessageWithPubKeys.message " + message);
if (message instanceof DisputeMessage)
dispatchMessage((DisputeMessage) message);
});
decryptedDirectMessageWithPubKeys.clear();
decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
Message message = decryptedMessageWithPubKey.message;
log.debug("decryptedMessageWithPubKey.message " + message);
if (message instanceof DisputeMessage) {
dispatchMessage((DisputeMessage) message);
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
}
});
decryptedMailboxMessageWithPubKeys.clear();
}
private void dispatchMessage(DisputeMessage message) {
if (message instanceof OpenNewDisputeMessage)
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
else if (message instanceof PeerOpenedDisputeMessage)
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
else if (message instanceof DisputeCommunicationMessage)
onDisputeDirectMessage((DisputeCommunicationMessage) message);
else if (message instanceof DisputeResultMessage)
onDisputeResultMessage((DisputeResultMessage) message);
else if (message instanceof PeerPublishedPayoutTxMessage)
onDisputedPayoutTxMessage((PeerPublishedPayoutTxMessage) message);
else
log.warn("Unsupported message at dispatchMessage.\nmessage=" + message);
}
public void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, ResultHandler resultHandler, FaultHandler faultHandler) {
if (!disputes.contains(dispute)) {
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
if (!storedDisputeOptional.isPresent() || reOpen) {
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);
if (!reOpen) {
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);
resultHandler.handleResult();
}
@Override
public void onStoredInMailbox() {
disputeCommunicationMessage.setStoredInMailbox(true);
resultHandler.handleResult();
}
@Override
public void onFault(String errorMessage) {
log.error("sendEncryptedMessage failed");
faultHandler.handleFault("Sending dispute message failed: " + errorMessage, new MessageDeliveryFailedException());
}
}
);
} else {
final String msg = "We got a dispute already open for that trade and trading peer.\n" +
"TradeId = " + dispute.getTradeId();
log.warn(msg);
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
}
} else {
final String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
log.warn(msg);
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
}
}
// arbitrator sends that to trading peer when he received openDispute request
private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener) {
Contract contractFromOpener = disputeFromOpener.getContract();
PubKeyRing pubKeyRing = disputeFromOpener.isDisputeOpenerIsBuyer() ? contractFromOpener.getSellerPubKeyRing() : contractFromOpener.getBuyerPubKeyRing();
Dispute dispute = new Dispute(
disputeStorage,
disputeFromOpener.getTradeId(),
pubKeyRing.hashCode(),
!disputeFromOpener.isDisputeOpenerIsBuyer(),
!disputeFromOpener.isDisputeOpenerIsOfferer(),
pubKeyRing,
disputeFromOpener.getTradeDate(),
contractFromOpener,
disputeFromOpener.getContractHash(),
disputeFromOpener.getDepositTxSerialized(),
disputeFromOpener.getPayoutTxSerialized(),
disputeFromOpener.getDepositTxId(),
disputeFromOpener.getPayoutTxId(),
disputeFromOpener.getContractAsJson(),
disputeFromOpener.getOffererContractSignature(),
disputeFromOpener.getTakerContractSignature(),
disputeFromOpener.getArbitratorPubKeyRing(),
disputeFromOpener.isSupportTicket()
);
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);
}
@Override
public void onStoredInMailbox() {
disputeCommunicationMessage.setStoredInMailbox(true);
}
@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)
public DisputeCommunicationMessage sendDisputeDirectMessage(Dispute dispute, String text, ArrayList<Attachment> attachments) {
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(dispute.getTradeId(),
dispute.getTraderPubKeyRing().hashCode(),
isTrader(dispute),
text,
p2PService.getAddress());
disputeCommunicationMessage.addAllAttachments(attachments);
PubKeyRing receiverPubKeyRing = null;
NodeAddress peerNodeAddress = null;
if (isTrader(dispute)) {
dispute.addDisputeMessage(disputeCommunicationMessage);
receiverPubKeyRing = dispute.getArbitratorPubKeyRing();
peerNodeAddress = dispute.getContract().arbitratorNodeAddress;
} else if (isArbitrator(dispute)) {
if (!disputeCommunicationMessage.isSystemMessage())
dispute.addDisputeMessage(disputeCommunicationMessage);
receiverPubKeyRing = dispute.getTraderPubKeyRing();
Contract contract = dispute.getContract();
if (contract.getBuyerPubKeyRing().equals(receiverPubKeyRing))
peerNodeAddress = contract.getBuyerNodeAddress();
else
peerNodeAddress = contract.getSellerNodeAddress();
} else {
log.error("That must not happen. Trader cannot communicate to other trader.");
}
if (receiverPubKeyRing != null) {
log.trace("sendDisputeDirectMessage to peerAddress " + peerNodeAddress);
p2PService.sendEncryptedMailboxMessage(peerNodeAddress,
receiverPubKeyRing,
disputeCommunicationMessage,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
disputeCommunicationMessage.setArrived(true);
}
@Override
public void onStoredInMailbox() {
disputeCommunicationMessage.setStoredInMailbox(true);
}
@Override
public void onFault(String errorMessage) {
log.error("sendEncryptedMessage failed");
}
}
);
}
return disputeCommunicationMessage;
}
// arbitrator send result to trader
public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) {
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(dispute.getTradeId(),
dispute.getTraderPubKeyRing().hashCode(),
false,
text,
p2PService.getAddress());
dispute.addDisputeMessage(disputeCommunicationMessage);
disputeResult.setDisputeCommunicationMessage(disputeCommunicationMessage);
NodeAddress peerNodeAddress;
Contract contract = dispute.getContract();
if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()))
peerNodeAddress = contract.getBuyerNodeAddress();
else
peerNodeAddress = contract.getSellerNodeAddress();
p2PService.sendEncryptedMailboxMessage(peerNodeAddress,
dispute.getTraderPubKeyRing(),
new DisputeResultMessage(disputeResult, p2PService.getAddress()),
new SendMailboxMessageListener() {
@Override
public void onArrived() {
disputeCommunicationMessage.setArrived(true);
}
@Override
public void onStoredInMailbox() {
disputeCommunicationMessage.setStoredInMailbox(true);
}
@Override
public void onFault(String errorMessage) {
log.error("sendEncryptedMessage failed");
}
}
);
}
// winner (or buyer in case of 50/50) sends tx to other peer
private void sendPeerPublishedPayoutTxMessage(Transaction transaction, Dispute dispute, Contract contract) {
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
NodeAddress peerNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
log.trace("sendPeerPublishedPayoutTxMessage to peerAddress " + peerNodeAddress);
p2PService.sendEncryptedMailboxMessage(peerNodeAddress,
peersPubKeyRing,
new PeerPublishedPayoutTxMessage(transaction.bitcoinSerialize(), dispute.getTradeId(), p2PService.getAddress()),
new SendMailboxMessageListener() {
@Override
public void onArrived() {
}
@Override
public void onStoredInMailbox() {
}
@Override
public void onFault(String errorMessage) {
log.error("sendEncryptedMessage failed");
}
}
);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Incoming message
///////////////////////////////////////////////////////////////////////////////////////////
// arbitrator receives that from trader who opens dispute
private void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) {
Dispute dispute = openNewDisputeMessage.dispute;
if (isArbitrator(dispute)) {
if (!disputes.contains(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());
}
} else {
log.error("Trader received openNewDisputeMessage. That must never happen.");
}
}
// not dispute requester receives that from arbitrator
private void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) {
Dispute dispute = peerOpenedDisputeMessage.dispute;
if (!isArbitrator(dispute)) {
if (!disputes.contains(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());
}
} else {
log.error("Arbitrator received peerOpenedDisputeMessage. That must never happen.");
}
}
// 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("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 disputeCommunicationMessage what we have already stored. TradeId = " + tradeId);
} else {
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);
}
}
}
// We get that message at both peers. The dispute object is in context of the trader
private void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) {
DisputeResult disputeResult = disputeResultMessage.disputeResult;
if (!isArbitrator(disputeResult)) {
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();
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());
dispute.setIsClosed(true);
if (dispute.disputeResultProperty().get() != null)
log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " +
"again because the first close did not succeed. TradeId = " + tradeId);
dispute.setDisputeResult(disputeResult);
// We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals
// There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb)
// The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives
// more BTC as he has deposited
final Contract contract = dispute.getContract();
boolean isBuyer = keyRing.getPubKeyRing().equals(contract.getBuyerPubKeyRing());
DisputeResult.Winner publisher = disputeResult.getWinner();
// Sometimes the user who receives the trade amount is never online, so we might want to
// let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer.
// Default isLoserPublisher is set to false
if (disputeResult.isLoserPublisher()) {
// we invert the logic
if (publisher == DisputeResult.Winner.BUYER)
publisher = DisputeResult.Winner.SELLER;
else if (publisher == DisputeResult.Winner.SELLER)
publisher = DisputeResult.Winner.BUYER;
}
if ((isBuyer && publisher == DisputeResult.Winner.BUYER)
|| (!isBuyer && publisher == DisputeResult.Winner.SELLER)
|| (isBuyer && publisher == DisputeResult.Winner.STALE_MATE)) {
final Optional<Trade> tradeOptional = tradeManager.getTradeById(tradeId);
Transaction payoutTx = null;
if (tradeOptional.isPresent()) {
payoutTx = tradeOptional.get().getPayoutTx();
} else {
final Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx();
}
}
if (payoutTx == null) {
if (dispute.getDepositTxSerialized() != null) {
try {
log.debug("do payout Transaction ");
AddressEntry multiSigAddressEntry = walletService.getOrCreateAddressEntry(dispute.getTradeId(), AddressEntry.Context.MULTI_SIG);
Transaction signedDisputedPayoutTx = tradeWalletService.traderSignAndFinalizeDisputedPayoutTx(
dispute.getDepositTxSerialized(),
disputeResult.getArbitratorSignature(),
disputeResult.getBuyerPayoutAmount(),
disputeResult.getSellerPayoutAmount(),
disputeResult.getArbitratorPayoutAmount(),
contract.getBuyerPayoutAddressString(),
contract.getSellerPayoutAddressString(),
disputeResult.getArbitratorAddressAsString(),
multiSigAddressEntry.getKeyPair(),
contract.getBuyerMultiSigPubKey(),
contract.getSellerMultiSigPubKey(),
disputeResult.getArbitratorPubKey()
);
Transaction committedDisputedPayoutTx = tradeWalletService.addTransactionToWallet(signedDisputedPayoutTx);
log.debug("broadcast committedDisputedPayoutTx");
tradeWalletService.broadcastTx(committedDisputedPayoutTx, new FutureCallback<Transaction>() {
@Override
public void onSuccess(Transaction transaction) {
log.debug("BroadcastTx succeeded. Transaction:" + transaction);
// after successful publish we send peer the tx
dispute.setDisputePayoutTxId(transaction.getHashAsString());
sendPeerPublishedPayoutTxMessage(transaction, dispute, contract);
// set state after payout as we call swapTradeEntryToAvailableEntry
if (tradeManager.getTradeById(dispute.getTradeId()).isPresent())
tradeManager.closeDisputedTrade(dispute.getTradeId());
else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(dispute.getTradeId());
if (openOfferOptional.isPresent())
openOfferManager.closeOpenOffer(openOfferOptional.get().getOffer());
}
}
@Override
public void onFailure(@NotNull Throwable t) {
log.error(t.getMessage());
}
});
} catch (AddressFormatException | WalletException | TransactionVerificationException e) {
e.printStackTrace();
log.error("Error at traderSignAndFinalizeDisputedPayoutTx " + e.getMessage());
}
} else {
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 = " + tradeId);
dispute.setDisputePayoutTxId(payoutTx.getHashAsString());
sendPeerPublishedPayoutTxMessage(payoutTx, dispute, contract);
}
} else {
log.trace("We don't publish the tx as we are not the winning party.");
// Clean up tangling trades
if (dispute.disputeResultProperty().get() != null &&
dispute.isClosed() &&
tradeManager.getTradeById(dispute.getTradeId()).isPresent())
tradeManager.closeDisputedTrade(dispute.getTradeId());
}
} else {
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 2 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.");
}
}
// losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer
private void onDisputedPayoutTxMessage(PeerPublishedPayoutTxMessage peerPublishedPayoutTxMessage) {
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);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
public Storage<DisputeList<Dispute>> getDisputeStorage() {
return disputeStorage;
}
public ObservableList<Dispute> getDisputesAsObservableList() {
return disputesObservableList;
}
public boolean isTrader(Dispute dispute) {
return keyRing.getPubKeyRing().equals(dispute.getTraderPubKeyRing());
}
private boolean isArbitrator(Dispute dispute) {
return keyRing.getPubKeyRing().equals(dispute.getArbitratorPubKeyRing());
}
private boolean isArbitrator(DisputeResult disputeResult) {
return disputeResult.getArbitratorAddressAsString().equals(walletService.getOrCreateAddressEntry(AddressEntry.Context.ARBITRATOR).getAddressString());
}
public String getNrOfDisputes(boolean isBuyer, Contract contract) {
return String.valueOf(getDisputesAsObservableList().stream()
.filter(e -> {
Contract contract1 = e.getContract();
if (contract1 == null)
return false;
if (isBuyer) {
NodeAddress buyerNodeAddress = contract1.getBuyerNodeAddress();
return buyerNodeAddress != null && buyerNodeAddress.equals(contract.getBuyerNodeAddress());
} else {
NodeAddress sellerNodeAddress = contract1.getSellerNodeAddress();
return sellerNodeAddress != null && sellerNodeAddress.equals(contract.getSellerNodeAddress());
}
})
.collect(Collectors.toSet()).size());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private Optional<Dispute> findDispute(String tradeId, int traderId) {
return disputes.stream().filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId).findAny();
}
public Optional<Dispute> findOwnDispute(String tradeId) {
return getDisputeStream(tradeId).findAny();
}
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();
}
}
}