mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 11:39:29 +00:00
support re-opening dispute if payout fails
This commit is contained in:
parent
79cd9f3e82
commit
3b0080dbba
9 changed files with 173 additions and 85 deletions
|
@ -112,7 +112,7 @@ public class CoreDisputesService {
|
||||||
|
|
||||||
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
||||||
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
||||||
disputeManager.sendDisputeOpenedMessage(dispute, false, resultHandler, faultHandler);
|
disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler);
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
}, trade.getId());
|
}, trade.getId());
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,8 +186,7 @@ public abstract class SupportManager {
|
||||||
private void onAckMessage(AckMessage ackMessage) {
|
private void onAckMessage(AckMessage ackMessage) {
|
||||||
if (ackMessage.getSourceType() == getAckMessageSourceType()) {
|
if (ackMessage.getSourceType() == getAckMessageSourceType()) {
|
||||||
if (ackMessage.isSuccess()) {
|
if (ackMessage.isSuccess()) {
|
||||||
log.info("Received AckMessage for {} with tradeId {} and uid {}",
|
log.info("Received AckMessage for {} with tradeId {} and uid {}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
|
||||||
|
|
||||||
// ack message on chat message received when dispute is opened and closed
|
// ack message on chat message received when dispute is opened and closed
|
||||||
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
|
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
|
||||||
|
@ -195,15 +194,35 @@ public abstract class SupportManager {
|
||||||
for (Dispute dispute : trade.getDisputes()) {
|
for (Dispute dispute : trade.getDisputes()) {
|
||||||
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||||
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||||
if (dispute.isClosed()) trade.pollWalletNormallyForMs(30000); // sync to check for payout
|
if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED) {
|
||||||
else trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
if (dispute.isClosed()) dispute.reOpen();
|
||||||
|
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
|
} else if (dispute.isClosed()) {
|
||||||
|
trade.pollWalletNormallyForMs(30000); // sync to check for payout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}",
|
log.warn("Received AckMessage with error state for {} with tradeId={}, sender={}, errorMessage={}",
|
||||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
|
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSenderNodeAddress(), ackMessage.getErrorMessage());
|
||||||
|
|
||||||
|
// nack message on chat message received when dispute closed message is nacked
|
||||||
|
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
|
||||||
|
Trade trade = tradeManager.getTrade(ackMessage.getSourceId());
|
||||||
|
for (Dispute dispute : trade.getDisputes()) {
|
||||||
|
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||||
|
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||||
|
if (trade.getDisputeState().isCloseRequested()) {
|
||||||
|
log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
|
||||||
|
dispute.setIsClosed();
|
||||||
|
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllChatMessages(ackMessage.getSourceId()).stream()
|
getAllChatMessages(ackMessage.getSourceId()).stream()
|
||||||
|
|
|
@ -77,6 +77,10 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||||
REOPENED,
|
REOPENED,
|
||||||
CLOSED;
|
CLOSED;
|
||||||
|
|
||||||
|
public boolean isOpen() {
|
||||||
|
return this == NEW || this == OPEN || this == REOPENED;
|
||||||
|
}
|
||||||
|
|
||||||
public static Dispute.State fromProto(protobuf.Dispute.State state) {
|
public static Dispute.State fromProto(protobuf.Dispute.State state) {
|
||||||
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
|
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,7 +326,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
|
|
||||||
// trader sends message to arbitrator to open dispute
|
// trader sends message to arbitrator to open dispute
|
||||||
public void sendDisputeOpenedMessage(Dispute dispute,
|
public void sendDisputeOpenedMessage(Dispute dispute,
|
||||||
boolean reOpen,
|
|
||||||
ResultHandler resultHandler,
|
ResultHandler resultHandler,
|
||||||
FaultHandler faultHandler) {
|
FaultHandler faultHandler) {
|
||||||
|
|
||||||
|
@ -356,7 +355,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
|
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||||
|
|
||||||
|
// add or re-open dispute
|
||||||
|
if (reOpen) {
|
||||||
|
dispute = storedDisputeOptional.get();
|
||||||
|
} else {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
}
|
||||||
|
|
||||||
String disputeInfo = getDisputeInfo(dispute);
|
String disputeInfo = getDisputeInfo(dispute);
|
||||||
String sysMsg = dispute.isSupportTicket() ?
|
String sysMsg = dispute.isSupportTicket() ?
|
||||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
|
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
|
||||||
|
@ -371,9 +379,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
p2PService.getAddress());
|
p2PService.getAddress());
|
||||||
chatMessage.setSystemMessage(true);
|
chatMessage.setSystemMessage(true);
|
||||||
dispute.addAndPersistChatMessage(chatMessage);
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
if (!reOpen) {
|
|
||||||
disputeList.add(dispute);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create dispute opened message
|
// create dispute opened message
|
||||||
trade.exportMultisigHex();
|
trade.exportMultisigHex();
|
||||||
|
@ -392,6 +397,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
|
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
|
||||||
|
|
||||||
// send dispute opened message
|
// send dispute opened message
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||||
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
||||||
dispute.getAgentPubKeyRing(),
|
dispute.getAgentPubKeyRing(),
|
||||||
disputeOpenedMessage,
|
disputeOpenedMessage,
|
||||||
|
@ -425,7 +431,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setStoredInMailbox(true);
|
chatMessage.setStoredInMailbox(true);
|
||||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
|
@ -442,6 +447,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setSendMessageError(errorMessage);
|
chatMessage.setSendMessageError(errorMessage);
|
||||||
|
trade.setDisputeState(Trade.DisputeState.NO_DISPUTE);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
faultHandler.handleFault("Sending dispute message failed: " +
|
faultHandler.handleFault("Sending dispute message failed: " +
|
||||||
errorMessage, new DisputeMessageDeliveryFailedException());
|
errorMessage, new DisputeMessageDeliveryFailedException());
|
||||||
|
@ -460,16 +466,30 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
|
|
||||||
// arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
|
// arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
|
||||||
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
|
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
|
||||||
Dispute dispute = message.getDispute();
|
Dispute msgDispute = message.getDispute();
|
||||||
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), msgDispute.getTradeId(), msgDispute.getId());
|
||||||
|
|
||||||
// get trade
|
// get trade
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
Trade trade = tradeManager.getTrade(msgDispute.getTradeId());
|
||||||
if (trade == null) {
|
if (trade == null) {
|
||||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
log.warn("Dispute trade {} does not exist", msgDispute.getTradeId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trade.isPayoutPublished()) {
|
||||||
|
log.warn("Dispute trade {} payout already published", msgDispute.getTradeId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find existing dispute
|
||||||
|
Optional<Dispute> storedDisputeOptional = findDispute(msgDispute);
|
||||||
|
|
||||||
|
// determine if re-opening dispute
|
||||||
|
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||||
|
|
||||||
|
// use existing dispute or create new
|
||||||
|
Dispute dispute = reOpen ? storedDisputeOptional.get() : msgDispute;
|
||||||
|
|
||||||
|
// process on trade thread
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade) {
|
||||||
String errorMessage = null;
|
String errorMessage = null;
|
||||||
|
@ -508,14 +528,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
}
|
}
|
||||||
|
|
||||||
// get sender
|
// get sender
|
||||||
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
TradePeer sender;
|
||||||
TradePeer sender = trade.getTradePeer(senderPubKeyRing);
|
if (reOpen) { // re-open can come from either peer
|
||||||
|
sender = trade.isArbitrator() ? trade.getTradePeer(message.getSenderNodeAddress()) : trade.getArbitrator();
|
||||||
|
senderPubKeyRing = sender.getPubKeyRing();
|
||||||
|
} else {
|
||||||
|
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
||||||
|
sender = trade.getTradePeer(senderPubKeyRing);
|
||||||
|
}
|
||||||
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||||
|
|
||||||
// update sender node address
|
// update sender node address
|
||||||
sender.setNodeAddress(message.getSenderNodeAddress());
|
sender.setNodeAddress(message.getSenderNodeAddress());
|
||||||
|
|
||||||
// message to trader is expected from arbitrator
|
// verify message to trader is expected from arbitrator
|
||||||
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
|
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
|
||||||
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
|
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
|
||||||
}
|
}
|
||||||
|
@ -533,16 +559,29 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// add chat message with price info
|
// add chat message with price info
|
||||||
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||||
|
|
||||||
// add dispute
|
// add or re-open dispute
|
||||||
synchronized (disputeList) {
|
synchronized (disputeList) {
|
||||||
if (!disputeList.contains(dispute)) {
|
if (!disputeList.contains(msgDispute)) {
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||||
if (!storedDisputeOptional.isPresent()) {
|
|
||||||
disputeList.add(dispute);
|
|
||||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
|
||||||
|
|
||||||
// send dispute opened message to peer if arbitrator
|
// update trade state
|
||||||
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
if (reOpen) {
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
|
} else {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset buyer and seller unsigned payout tx hex
|
||||||
|
trade.getBuyer().setUnsignedPayoutTxHex(null);
|
||||||
|
trade.getSeller().setUnsignedPayoutTxHex(null);
|
||||||
|
|
||||||
|
// send dispute opened message to other peer if arbitrator
|
||||||
|
if (trade.isArbitrator()) {
|
||||||
|
TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker();
|
||||||
|
if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress());
|
||||||
|
sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
||||||
|
}
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
} else {
|
} else {
|
||||||
|
@ -553,7 +592,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// add chat message with mediation info if applicable
|
// add chat message with mediation info if applicable
|
||||||
addMediationResultMessage(dispute);
|
addMediationResultMessage(dispute);
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId());
|
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + msgDispute.getTradeId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -566,7 +605,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
// use chat message instead of open dispute message for the ack
|
// use chat message instead of open dispute message for the ack
|
||||||
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
|
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
|
||||||
if (!messages.isEmpty()) {
|
if (!messages.isEmpty()) {
|
||||||
ChatMessage msg = messages.get(0);
|
ChatMessage msg = messages.get(messages.size() - 1); // send ack to sender of last chat message
|
||||||
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
|
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -580,7 +619,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
Contract contractFromOpener,
|
Contract contractFromOpener,
|
||||||
PubKeyRing pubKeyRing,
|
PubKeyRing pubKeyRing,
|
||||||
String updatedMultisigHex) {
|
String updatedMultisigHex) {
|
||||||
log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
|
log.info("{} sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
|
||||||
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
|
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
|
||||||
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
|
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
|
||||||
// message and not skip the system message of the peer as it would be the case if we have created the system msg
|
// message and not skip the system message of the peer as it would be the case if we have created the system msg
|
||||||
|
@ -602,6 +641,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create mirrored dispute
|
||||||
Dispute dispute = new Dispute(new Date().getTime(),
|
Dispute dispute = new Dispute(new Date().getTime(),
|
||||||
disputeFromOpener.getTradeId(),
|
disputeFromOpener.getTradeId(),
|
||||||
pubKeyRing.hashCode(),
|
pubKeyRing.hashCode(),
|
||||||
|
@ -627,10 +667,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
||||||
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
||||||
|
|
||||||
|
// skip if dispute already open
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
|
if (storedDisputeOptional.isPresent() && !storedDisputeOptional.get().isClosed()) {
|
||||||
// Valid case if both have opened a dispute and agent was not online.
|
|
||||||
if (storedDisputeOptional.isPresent()) {
|
|
||||||
log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId());
|
log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -652,8 +691,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
|
|
||||||
addPriceInfoMessage(dispute, 0);
|
addPriceInfoMessage(dispute, 0);
|
||||||
|
|
||||||
synchronized (disputeList) {
|
// add or re-open dispute
|
||||||
disputeList.add(dispute);
|
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||||
|
if (reOpen) {
|
||||||
|
dispute = storedDisputeOptional.get();
|
||||||
|
dispute.reOpen();
|
||||||
|
} else {
|
||||||
|
synchronized (disputeList) {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get trade
|
// get trade
|
||||||
|
@ -663,10 +709,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We mirrored dispute already!
|
// create dispute opened message with peer dispute
|
||||||
Contract contract = dispute.getContract();
|
TradePeer peer = trade.getTradePeer(pubKeyRing);
|
||||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
PubKeyRing peersPubKeyRing = peer.getPubKeyRing();
|
||||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
|
NodeAddress peersNodeAddress = peer.getNodeAddress();
|
||||||
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
|
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
|
@ -754,7 +800,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
dispute.addAndPersistChatMessage(chatMessage);
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create dispute payout tx once per trader if we have their updated multisig hex
|
// create dispute payout tx
|
||||||
TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing());
|
TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing());
|
||||||
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) {
|
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) {
|
||||||
createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
|
createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
|
||||||
|
@ -906,8 +952,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
if (updateState) {
|
if (updateState) {
|
||||||
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
|
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
|
||||||
trade.updatePayout(payoutTx);
|
trade.updatePayout(payoutTx);
|
||||||
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||||
if (trade.getSeller().getUpdatedMultisigHex() != null && trade.getSeller().getUnsignedPayoutTxHex() == null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||||
}
|
}
|
||||||
trade.requestPersistence();
|
trade.requestPersistence();
|
||||||
return payoutTx;
|
return payoutTx;
|
||||||
|
@ -942,21 +988,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
|
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Dispute> findDispute(Dispute dispute) {
|
public Optional<Dispute> findDispute(Dispute dispute) {
|
||||||
return findDispute(dispute.getTradeId(), dispute.getTraderId());
|
return findDispute(dispute.getTradeId(), dispute.getTraderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Optional<Dispute> findDispute(DisputeResult disputeResult) {
|
public Optional<Dispute> findDispute(DisputeResult disputeResult) {
|
||||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||||
return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId());
|
return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Dispute> findDispute(ChatMessage message) {
|
public Optional<Dispute> findDispute(ChatMessage message) {
|
||||||
return findDispute(message.getTradeId(), message.getTraderId());
|
return findDispute(message.getTradeId(), message.getTraderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Optional<Dispute> findDispute(String tradeId, int traderId) {
|
public Optional<Dispute> findDispute(String tradeId, int traderId) {
|
||||||
T disputeList = getDisputeList();
|
T disputeList = getDisputeList();
|
||||||
if (disputeList == null) {
|
if (disputeList == null) {
|
||||||
log.warn("disputes is null");
|
log.warn("disputes is null");
|
||||||
|
|
|
@ -308,7 +308,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
if (trade.isPayoutPublished()) {
|
if (trade.isPayoutPublished()) {
|
||||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
} else {
|
} else {
|
||||||
if (e instanceof IllegalArgumentException) throw e;
|
if (e instanceof IllegalArgumentException || e instanceof IllegalStateException) throw e;
|
||||||
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e);
|
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -328,14 +328,18 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||||
requestPersistence(trade);
|
requestPersistence(trade);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error processing dispute closed message: " + e.getMessage());
|
log.warn("Error processing dispute closed message: {}", e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
requestPersistence(trade);
|
requestPersistence(trade);
|
||||||
|
|
||||||
// nack bad message and do not reprocess
|
// nack bad message and do not reprocess
|
||||||
if (e instanceof IllegalArgumentException) {
|
if (e instanceof IllegalArgumentException || e instanceof IllegalStateException) {
|
||||||
trade.getArbitrator().setDisputeClosedMessage(null); // message is processed
|
trade.getArbitrator().setDisputeClosedMessage(null); // message is processed
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
|
String warningMsg = "Error processing dispute closed message: " + e.getMessage() + "\n\nOpen another dispute to try again (ctrl+o).";
|
||||||
|
trade.prependErrorMessage(warningMsg);
|
||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage());
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage());
|
||||||
|
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
|
||||||
requestPersistence(trade);
|
requestPersistence(trade);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -442,12 +446,16 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
|
|
||||||
// sign arbitrator-signed payout tx
|
// sign arbitrator-signed payout tx
|
||||||
if (trade.getPayoutTxHex() == null) {
|
if (trade.getPayoutTxHex() == null) {
|
||||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
|
try {
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
|
||||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||||
disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
|
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||||
trade.setPayoutTxHex(signedMultisigTxHex);
|
disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||||
requestPersistence(trade);
|
trade.setPayoutTxHex(signedMultisigTxHex);
|
||||||
|
requestPersistence(trade);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
|
||||||
// verify mining fee is within tolerance by recreating payout tx
|
// verify mining fee is within tolerance by recreating payout tx
|
||||||
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
||||||
|
|
|
@ -321,7 +321,11 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOpen() {
|
public boolean isOpen() {
|
||||||
return this == DisputeState.DISPUTE_OPENED;
|
return isRequested() && !isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCloseRequested() {
|
||||||
|
return this.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isClosed() {
|
public boolean isClosed() {
|
||||||
|
|
|
@ -37,7 +37,6 @@ import haveno.core.offer.OfferUtil;
|
||||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||||
import haveno.core.support.SupportType;
|
import haveno.core.support.SupportType;
|
||||||
import haveno.core.support.dispute.Dispute;
|
import haveno.core.support.dispute.Dispute;
|
||||||
import haveno.core.support.dispute.DisputeAlreadyOpenException;
|
|
||||||
import haveno.core.support.dispute.DisputeList;
|
import haveno.core.support.dispute.DisputeList;
|
||||||
import haveno.core.support.dispute.DisputeManager;
|
import haveno.core.support.dispute.DisputeManager;
|
||||||
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
||||||
|
@ -67,6 +66,7 @@ import haveno.network.p2p.P2PService;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
@ -545,33 +545,38 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||||
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
||||||
|
|
||||||
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
|
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
|
||||||
sendDisputeOpenedMessage(dispute, false, disputeManager);
|
sendDisputeOpenedMessage(dispute, disputeManager);
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
} else if (useArbitration) {
|
} else if (useArbitration) {
|
||||||
// Only if we have completed mediation we allow arbitration
|
// Only if we have completed mediation we allow arbitration
|
||||||
disputeManager = arbitrationManager;
|
disputeManager = arbitrationManager;
|
||||||
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
|
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
|
||||||
trade.exportMultisigHex();
|
trade.exportMultisigHex();
|
||||||
sendDisputeOpenedMessage(dispute, false, disputeManager);
|
sendDisputeOpenedMessage(dispute, disputeManager);
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
} else {
|
} else {
|
||||||
log.warn("Invalid dispute state {}", disputeState.name());
|
log.warn("Invalid dispute state {}", disputeState.name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendDisputeOpenedMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
|
private void sendDisputeOpenedMessage(Dispute dispute, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
|
||||||
disputeManager.sendDisputeOpenedMessage(dispute, reOpen,
|
Optional<Dispute> optionalDispute = disputeManager.findDispute(dispute);
|
||||||
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> {
|
boolean disputeClosed = optionalDispute.isPresent() && optionalDispute.get().isClosed();
|
||||||
if ((throwable instanceof DisputeAlreadyOpenException)) {
|
if (disputeClosed) {
|
||||||
errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg");
|
String msg = "We got a dispute already open for that trade and trading peer.\n" + "TradeId = " + dispute.getTradeId();
|
||||||
new Popup().warning(errorMessage)
|
new Popup().warning(msg + "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"))
|
||||||
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
|
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
|
||||||
.onAction(() -> sendDisputeOpenedMessage(dispute, true, disputeManager))
|
.onAction(() -> doSendDisputeOpenedMessage(dispute, disputeManager))
|
||||||
.closeButtonText(Res.get("shared.cancel")).show();
|
.closeButtonText(Res.get("shared.cancel")).show();
|
||||||
} else {
|
} else {
|
||||||
new Popup().warning(errorMessage).show();
|
doSendDisputeOpenedMessage(dispute, disputeManager);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private void doSendDisputeOpenedMessage(Dispute dispute, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
|
||||||
|
disputeManager.sendDisputeOpenedMessage(dispute,
|
||||||
|
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class),
|
||||||
|
(errorMessage, throwable) -> new Popup().warning(errorMessage).show());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isReadyForTxBroadcast() {
|
public boolean isReadyForTxBroadcast() {
|
||||||
|
|
|
@ -190,15 +190,13 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
}
|
}
|
||||||
trade.errorMessageProperty().addListener(errorMessageListener);
|
trade.errorMessageProperty().addListener(errorMessageListener);
|
||||||
|
|
||||||
if (!isMediationClosedState()) {
|
tradeStepInfo.setOnAction(e -> {
|
||||||
tradeStepInfo.setOnAction(e -> {
|
if (!isArbitrationOpenedState() && this.isTradePeriodOver()) {
|
||||||
if (this.isTradePeriodOver()) {
|
openSupportTicket();
|
||||||
openSupportTicket();
|
} else {
|
||||||
} else {
|
openChat();
|
||||||
openChat();
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// We get mailbox messages processed after we have bootstrapped. This will influence the states we
|
// We get mailbox messages processed after we have bootstrapped. This will influence the states we
|
||||||
// handle in our disputeStateSubscription and mediationResultStateSubscriptions. To avoid that we show
|
// handle in our disputeStateSubscription and mediationResultStateSubscriptions. To avoid that we show
|
||||||
|
@ -572,7 +570,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) {
|
private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) {
|
||||||
if (isInArbitration()) {
|
if (isInMediation()) {
|
||||||
if (isRefundRequestStartedByPeer()) {
|
if (isRefundRequestStartedByPeer()) {
|
||||||
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
|
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
|
||||||
} else if (isRefundRequestSelfStarted()) {
|
} else if (isRefundRequestSelfStarted()) {
|
||||||
|
@ -597,7 +595,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isInArbitration() {
|
private boolean isInMediation() {
|
||||||
return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted();
|
return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -613,6 +611,10 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED;
|
return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isArbitrationOpenedState() {
|
||||||
|
return trade.getDisputeState().isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTradePeriodOver() {
|
private boolean isTradePeriodOver() {
|
||||||
return Trade.TradePeriodState.TRADE_PERIOD_OVER == trade.tradePeriodStateProperty().get();
|
return Trade.TradePeriodState.TRADE_PERIOD_OVER == trade.tradePeriodStateProperty().get();
|
||||||
}
|
}
|
||||||
|
@ -741,7 +743,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) {
|
private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) {
|
||||||
if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) {
|
if (!trade.getDisputeState().isOpen()) {
|
||||||
switch (tradePeriodState) {
|
switch (tradePeriodState) {
|
||||||
case FIRST_HALF:
|
case FIRST_HALF:
|
||||||
// just for dev testing. not possible to go back in time ;-)
|
// just for dev testing. not possible to go back in time ;-)
|
||||||
|
|
|
@ -1485,7 +1485,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCloseDisputeFromChatWindow(Dispute dispute) {
|
public void onCloseDisputeFromChatWindow(Dispute dispute) {
|
||||||
if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState() == Dispute.State.OPEN) {
|
if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState().isOpen()) {
|
||||||
handleOnProcessDispute(dispute);
|
handleOnProcessDispute(dispute);
|
||||||
} else {
|
} else {
|
||||||
closeDisputeFromButton();
|
closeDisputeFromButton();
|
||||||
|
|
Loading…
Reference in a new issue