support re-opening dispute if payout fails

This commit is contained in:
woodser 2024-07-31 19:36:15 -04:00
parent 79cd9f3e82
commit 3b0080dbba
9 changed files with 173 additions and 85 deletions

View file

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

View file

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

View file

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

View file

@ -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
TradePeer sender;
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(); senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
TradePeer sender = trade.getTradePeer(senderPubKeyRing); 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()) {
// update trade state
if (reOpen) {
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
} else {
disputeList.add(dispute); disputeList.add(dispute);
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
}
// send dispute opened message to peer if arbitrator // reset buyer and seller unsigned payout tx hex
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex()); 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,9 +691,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
addPriceInfoMessage(dispute, 0); addPriceInfoMessage(dispute, 0);
// add or re-open dispute
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
if (reOpen) {
dispute = storedDisputeOptional.get();
dispute.reOpen();
} else {
synchronized (disputeList) { synchronized (disputeList) {
disputeList.add(dispute); disputeList.add(dispute);
} }
}
// get trade // get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId()); Trade trade = tradeManager.getTrade(dispute.getTradeId());
@ -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");

View file

@ -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) {
try {
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex); MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx"); if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex(); String signedMultisigTxHex = result.getSignedMultisigTxHex();
disputeTxSet.setMultisigTxHex(signedMultisigTxHex); disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
trade.setPayoutTxHex(signedMultisigTxHex); trade.setPayoutTxHex(signedMultisigTxHex);
requestPersistence(trade); 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?

View file

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

View file

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

View file

@ -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 (this.isTradePeriodOver()) { if (!isArbitrationOpenedState() && 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 ;-)

View file

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