mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-09-27 12:00:59 -04:00
refactor dispute preparation and requesting off main thread
This commit is contained in:
parent
4e188a9343
commit
06f472dc53
35 changed files with 416 additions and 246 deletions
|
@ -22,7 +22,6 @@ import com.google.inject.Inject;
|
|||
import com.google.inject.Singleton;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.crypto.KeyRing;
|
||||
import haveno.common.crypto.PubKeyRing;
|
||||
import haveno.common.handlers.FaultHandler;
|
||||
|
@ -101,57 +100,51 @@ public class CoreDisputesService {
|
|||
public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) {
|
||||
Trade trade = tradeManager.getOpenTrade(tradeId).orElseThrow(() ->
|
||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
||||
Offer offer = trade.getOffer();
|
||||
if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId));
|
||||
|
||||
// open dispute on trade thread
|
||||
ThreadUtils.execute(() -> {
|
||||
Offer offer = trade.getOffer();
|
||||
if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId));
|
||||
// Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded
|
||||
// to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored.
|
||||
var disputeManager = arbitrationManager;
|
||||
var isSupportTicket = false;
|
||||
var isMaker = tradeManager.isMyOffer(offer);
|
||||
var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket);
|
||||
|
||||
// Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded
|
||||
// to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored.
|
||||
var disputeManager = arbitrationManager;
|
||||
var isSupportTicket = false;
|
||||
var isMaker = tradeManager.isMyOffer(offer);
|
||||
var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket);
|
||||
|
||||
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
||||
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
||||
disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler);
|
||||
tradeManager.requestPersistence();
|
||||
}, trade.getId());
|
||||
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
||||
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
||||
disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler);
|
||||
tradeManager.requestPersistence();
|
||||
}
|
||||
|
||||
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
|
||||
synchronized (trade.getLock()) {
|
||||
byte[] payoutTxSerialized = null;
|
||||
String payoutTxHashAsString = null;
|
||||
byte[] payoutTxSerialized = null;
|
||||
String payoutTxHashAsString = null;
|
||||
|
||||
PubKeyRing arbitratorPubKeyRing = trade.getArbitrator().getPubKeyRing();
|
||||
checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null");
|
||||
Dispute dispute = new Dispute(new Date().getTime(),
|
||||
trade.getId(),
|
||||
pubKey.hashCode(), // trader id,
|
||||
true,
|
||||
(offer.getDirection() == OfferDirection.BUY) == isMaker,
|
||||
isMaker,
|
||||
pubKey,
|
||||
trade.getDate().getTime(),
|
||||
trade.getMaxTradePeriodDate().getTime(),
|
||||
trade.getContract(),
|
||||
trade.getContractHash(),
|
||||
payoutTxSerialized,
|
||||
payoutTxHashAsString,
|
||||
trade.getContractAsJson(),
|
||||
trade.getMaker().getContractSignature(),
|
||||
trade.getTaker().getContractSignature(),
|
||||
trade.getMaker().getPaymentAccountPayload(),
|
||||
trade.getTaker().getPaymentAccountPayload(),
|
||||
arbitratorPubKeyRing,
|
||||
isSupportTicket,
|
||||
SupportType.ARBITRATION);
|
||||
PubKeyRing arbitratorPubKeyRing = trade.getArbitrator().getPubKeyRing();
|
||||
checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null");
|
||||
Dispute dispute = new Dispute(new Date().getTime(),
|
||||
trade.getId(),
|
||||
pubKey.hashCode(), // trader id,
|
||||
true,
|
||||
(offer.getDirection() == OfferDirection.BUY) == isMaker,
|
||||
isMaker,
|
||||
pubKey,
|
||||
trade.getDate().getTime(),
|
||||
trade.getMaxTradePeriodDate().getTime(),
|
||||
trade.getContract(),
|
||||
trade.getContractHash(),
|
||||
payoutTxSerialized,
|
||||
payoutTxHashAsString,
|
||||
trade.getContractAsJson(),
|
||||
trade.getMaker().getContractSignature(),
|
||||
trade.getTaker().getContractSignature(),
|
||||
trade.getMaker().getPaymentAccountPayload(),
|
||||
trade.getTaker().getPaymentAccountPayload(),
|
||||
arbitratorPubKeyRing,
|
||||
isSupportTicket,
|
||||
SupportType.ARBITRATION);
|
||||
|
||||
return dispute;
|
||||
}
|
||||
return dispute;
|
||||
}
|
||||
|
||||
// TODO: does not wait for success or error response
|
||||
|
@ -313,6 +306,7 @@ public class CoreDisputesService {
|
|||
Dispute dispute;
|
||||
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
|
||||
else throw new IllegalStateException(format("dispute with id '%s' not found", disputeId));
|
||||
if (!arbitrationManager.canSendChatMessages(dispute)) throw new IllegalStateException(format("dispute with id '%s' cannot send chat messages (must be open or stored to mailbox)", disputeId));
|
||||
ChatMessage chatMessage = new ChatMessage(
|
||||
arbitrationManager.getSupportType(),
|
||||
dispute.getTradeId(),
|
||||
|
|
|
@ -122,6 +122,8 @@ public abstract class SupportManager {
|
|||
|
||||
public abstract void requestPersistence();
|
||||
|
||||
public abstract void persistNow(@Nullable Runnable completeHandler);
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Delegates p2pService
|
||||
|
@ -195,7 +197,7 @@ public abstract class SupportManager {
|
|||
synchronized (dispute.getChatMessages()) {
|
||||
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||
if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED) {
|
||||
if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_PREPARING || trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED) { // ack can arrive before saw arrived
|
||||
if (dispute.isClosed()) dispute.reOpen();
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||
} else if (dispute.isClosed()) {
|
||||
|
@ -217,14 +219,10 @@ public abstract class SupportManager {
|
|||
synchronized (dispute.getChatMessages()) {
|
||||
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||
if (trade.getDisputeState().isRequested()) {
|
||||
if (!trade.isArbitrator() && (trade.getDisputeState().isRequested() || trade.getDisputeState().isCloseRequested())) {
|
||||
log.warn("DisputeOpenedMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
|
||||
dispute.setIsClosed();
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||
} else 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -243,6 +241,9 @@ public abstract class SupportManager {
|
|||
msg.setAckError(ackMessage.getErrorMessage());
|
||||
});
|
||||
});
|
||||
|
||||
tradeManager.persistNow(null);
|
||||
persistNow(null);
|
||||
requestPersistence();
|
||||
}
|
||||
}
|
||||
|
@ -266,7 +267,7 @@ public abstract class SupportManager {
|
|||
|
||||
mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress,
|
||||
receiverPubKeyRing,
|
||||
message,
|
||||
message.copy(),
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
|
|
|
@ -159,14 +159,12 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||
EasyBind.subscribe(dispute.getBadgeCountProperty(),
|
||||
isAlerting -> {
|
||||
// We get the event before the list gets updated, so we execute on next frame
|
||||
UserThread.execute(() -> {
|
||||
synchronized (disputeList.getObservableList()) {
|
||||
int numAlerts = (int) disputeList.getList().stream()
|
||||
.mapToLong(x -> x.getBadgeCountProperty().getValue())
|
||||
.sum();
|
||||
numOpenDisputes.set(numAlerts);
|
||||
}
|
||||
});
|
||||
synchronized (disputeList.getObservableList()) {
|
||||
int numAlerts = (int) disputeList.getList().stream()
|
||||
.mapToLong(x -> x.getBadgeCountProperty().getValue())
|
||||
.sum();
|
||||
UserThread.execute(() -> numOpenDisputes.set(numAlerts));
|
||||
}
|
||||
});
|
||||
disputedTradeIds.add(dispute.getTradeId());
|
||||
});
|
||||
|
@ -176,4 +174,8 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||
public void requestPersistence() {
|
||||
persistenceManager.requestPersistence();
|
||||
}
|
||||
|
||||
public void persistNow(@Nullable Runnable completeHandler) {
|
||||
persistenceManager.persistNow(completeHandler);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ import haveno.core.trade.Contract;
|
|||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.SellerTrade;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.Trade.DisputeState;
|
||||
import haveno.core.trade.TradeManager;
|
||||
import haveno.core.trade.protocol.TradePeer;
|
||||
import haveno.core.xmr.wallet.Restrictions;
|
||||
|
@ -158,6 +159,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
|
||||
@Override
|
||||
public void requestPersistence() {
|
||||
tradeManager.requestPersistence();
|
||||
disputeListService.requestPersistence();
|
||||
}
|
||||
|
||||
|
@ -166,6 +168,12 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
disputeListService.requestPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persistNow(@Nullable Runnable completeHandler) {
|
||||
tradeManager.persistNow(null);
|
||||
disputeListService.persistNow(completeHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
||||
Optional<Dispute> disputeOptional = findDispute(message);
|
||||
|
@ -347,6 +355,36 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
return;
|
||||
}
|
||||
|
||||
// update dispute state
|
||||
trade.setDisputeState(Trade.DisputeState.DISPUTE_PREPARING);
|
||||
|
||||
// open dispute on trade thread
|
||||
ThreadUtils.execute(() -> {
|
||||
|
||||
// try to send dispute
|
||||
try {
|
||||
sendDisputeOpenedMessageAux(trade, dispute, resultHandler, (errorMessage, throwable) -> {
|
||||
log.warn("Failed to open dispute for trade: " + dispute.getTradeId() + ": " + errorMessage, throwable);
|
||||
removeDisputes(trade);
|
||||
faultHandler.handleFault(errorMessage, throwable);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
String errorMsg = "Failed to open dispute for trade " + dispute.getTradeId() + ": " + e.getMessage();
|
||||
log.error(errorMsg, e);
|
||||
removeDisputes(trade);
|
||||
faultHandler.handleFault(errorMsg, e);
|
||||
}
|
||||
}, trade.getId());
|
||||
}
|
||||
|
||||
private void sendDisputeOpenedMessageAux(Trade trade,
|
||||
Dispute dispute,
|
||||
ResultHandler resultHandler,
|
||||
FaultHandler faultHandler) {
|
||||
log.info("Opening dispute for {} {}, dispute {}",
|
||||
DisputeOpenedMessage.class.getSimpleName(), trade.getClass().getSimpleName(),
|
||||
dispute.getTradeId(), dispute.getId());
|
||||
|
||||
// arbitrator cannot open disputes
|
||||
if (trade.isArbitrator()) {
|
||||
String errorMsg = "Arbitrators cannot open disputes.";
|
||||
|
@ -354,13 +392,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
return;
|
||||
}
|
||||
|
||||
log.info("Sending {} for {} {}, dispute {}",
|
||||
DisputeOpenedMessage.class.getSimpleName(), trade.getClass().getSimpleName(),
|
||||
dispute.getTradeId(), dispute.getId());
|
||||
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
// verify deposits unlocked or one is missing
|
||||
if (trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal() && !trade.isDepositTxMissing()) {
|
||||
String errorMsg = Res.get("portfolio.pending.error.depositTxNotConfirmed");
|
||||
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -371,6 +406,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
return;
|
||||
}
|
||||
|
||||
// set dispute
|
||||
T disputeList = getDisputeList();
|
||||
synchronized (disputeList.getObservableList()) {
|
||||
if (disputeList.contains(dispute)) {
|
||||
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
|
||||
|
@ -388,114 +425,144 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
} else {
|
||||
disputeList.add(dispute);
|
||||
}
|
||||
|
||||
String disputeInfo = getDisputeInfo(dispute);
|
||||
String sysMsg = dispute.isSupportTicket() ?
|
||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
|
||||
Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
|
||||
|
||||
ChatMessage chatMessage = new ChatMessage(
|
||||
getSupportType(),
|
||||
dispute.getTradeId(),
|
||||
keyRing.getPubKeyRing().hashCode(),
|
||||
false,
|
||||
Res.get("support.systemMsg", sysMsg),
|
||||
p2PService.getAddress());
|
||||
chatMessage.setSystemMessage(true);
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
|
||||
// try to import latest multisig info
|
||||
try {
|
||||
trade.importMultisigHex();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to import multisig hex", e);
|
||||
}
|
||||
|
||||
// try to export latest multisig info
|
||||
try {
|
||||
trade.exportMultisigHex();
|
||||
if (trade instanceof SellerTrade) {
|
||||
trade.getProcessModel().setPaymentSentPayoutTxStale(true); // exporting multisig hex will invalidate previously unsigned payout txs
|
||||
trade.getSelf().setUnsignedPayoutTxHex(null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to export multisig hex", e);
|
||||
}
|
||||
|
||||
// create dispute opened message
|
||||
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
||||
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType(),
|
||||
trade.getSelf().getUpdatedMultisigHex(),
|
||||
trade.getArbitrator().getPaymentSentMessage());
|
||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
|
||||
|
||||
// send dispute opened message
|
||||
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
||||
dispute.getAgentPubKeyRing(),
|
||||
disputeOpenedMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
clearPendingMessage();
|
||||
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setArrived(true);
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||
requestPersistence();
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
clearPendingMessage();
|
||||
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setStoredInMailbox(true);
|
||||
requestPersistence();
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}, errorMessage={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||
chatMessage.getUid(), errorMessage);
|
||||
|
||||
clearPendingMessage();
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setSendMessageError(errorMessage);
|
||||
trade.setDisputeState(Trade.DisputeState.NO_DISPUTE);
|
||||
requestPersistence();
|
||||
faultHandler.handleFault("Sending dispute message failed: " +
|
||||
errorMessage, new DisputeMessageDeliveryFailedException());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// save state
|
||||
persistNow(null);
|
||||
|
||||
String disputeInfo = getDisputeInfo(dispute);
|
||||
String sysMsg = dispute.isSupportTicket() ?
|
||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
|
||||
Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
|
||||
|
||||
ChatMessage chatMessage = new ChatMessage(
|
||||
getSupportType(),
|
||||
dispute.getTradeId(),
|
||||
keyRing.getPubKeyRing().hashCode(),
|
||||
false,
|
||||
Res.get("support.systemMsg", sysMsg),
|
||||
p2PService.getAddress());
|
||||
chatMessage.setSystemMessage(true);
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
|
||||
// try to import latest multisig info
|
||||
try {
|
||||
trade.importMultisigHex();
|
||||
} catch (Exception e) {
|
||||
if (!trade.isShutDownStarted()) log.error("Failed to import multisig hex", e);
|
||||
}
|
||||
|
||||
// abort if shutting down
|
||||
if (trade.isShutDownStarted()) {
|
||||
String errorMsg = "Aborting opening dispute for " + trade.getClass().getSimpleName() + " " + trade.getId() + " because shut down is started";
|
||||
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
// try to export latest multisig info
|
||||
try {
|
||||
trade.exportMultisigHex();
|
||||
if (trade instanceof SellerTrade) {
|
||||
trade.getProcessModel().setPaymentSentPayoutTxStale(true); // exporting multisig hex will invalidate previously unsigned payout txs
|
||||
trade.getSelf().setUnsignedPayoutTxHex(null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (!trade.isShutDownStarted()) log.error("Failed to export multisig hex", e);
|
||||
}
|
||||
|
||||
// abort if shutting down
|
||||
if (trade.isShutDownStarted()) {
|
||||
String errorMsg = "Aborting opening dispute for " + trade.getClass().getSimpleName() + " " + trade.getId() + " because shut down is started";
|
||||
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
// create dispute opened message
|
||||
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
||||
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType(),
|
||||
trade.getSelf().getUpdatedMultisigHex(),
|
||||
trade.getArbitrator().getPaymentSentMessage());
|
||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
|
||||
|
||||
// send dispute opened message
|
||||
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
||||
dispute.getAgentPubKeyRing(),
|
||||
disputeOpenedMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
clearPendingMessage();
|
||||
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setArrived(true);
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||
persistNow(null);
|
||||
if (resultHandler != null) resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
clearPendingMessage();
|
||||
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setStoredInMailbox(true);
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||
trade.persistNow(null);
|
||||
persistNow(null);
|
||||
if (resultHandler != null) resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}, errorMessage={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(),
|
||||
chatMessage.getUid(), errorMessage);
|
||||
|
||||
clearPendingMessage();
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setSendMessageError(errorMessage);
|
||||
trade.setDisputeState(Trade.DisputeState.NO_DISPUTE);
|
||||
persistNow(null);
|
||||
faultHandler.handleFault("Sending dispute message failed: " +
|
||||
errorMessage, new DisputeMessageDeliveryFailedException());
|
||||
}
|
||||
});
|
||||
|
||||
persistNow(null);
|
||||
}
|
||||
|
||||
public void removeDisputes(Trade trade) {
|
||||
T disputeList = getDisputeList();
|
||||
synchronized (disputeList.getObservableList()) {
|
||||
for (Dispute dispute : trade.getDisputes()) {
|
||||
disputeList.remove(dispute);
|
||||
}
|
||||
}
|
||||
trade.setDisputeState(Trade.DisputeState.NO_DISPUTE);
|
||||
clearPendingMessage();
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
|
@ -793,7 +860,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
// We use the chatMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setArrived(true);
|
||||
requestPersistence();
|
||||
persistNow(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -808,7 +875,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
// We use the chatMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setStoredInMailbox(true);
|
||||
requestPersistence();
|
||||
persistNow(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -823,11 +890,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
// We use the chatMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setSendMessageError(errorMessage);
|
||||
requestPersistence();
|
||||
persistNow(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
requestPersistence();
|
||||
persistNow(null);
|
||||
}
|
||||
|
||||
// arbitrator sends result to trader when their dispute is closed
|
||||
|
@ -1110,6 +1177,24 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
return retVal;
|
||||
}
|
||||
|
||||
public boolean canSendChatMessages(Dispute dispute) {
|
||||
Optional<Trade> tradeOptional = findTrade(dispute);
|
||||
if (!tradeOptional.isPresent()) {
|
||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||
return false;
|
||||
}
|
||||
Trade trade = tradeOptional.get();
|
||||
if (trade.isPayoutPublished()) return false;
|
||||
if (trade.getDisputeState() == DisputeState.DISPUTE_REQUESTED) {
|
||||
for (ChatMessage msg : dispute.getChatMessages()) {
|
||||
if (Boolean.TRUE.equals(msg.getStoredInMailboxProperty().get())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return trade.getDisputeState().isOpen();
|
||||
}
|
||||
|
||||
private void addMediationResultMessage(Dispute dispute) {
|
||||
// In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent.
|
||||
if (dispute.getMediatorsDisputeResult() != null) {
|
||||
|
|
|
@ -67,6 +67,7 @@ import haveno.core.trade.Contract;
|
|||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.TradeManager;
|
||||
import haveno.core.trade.Trade.DisputeState;
|
||||
import haveno.core.trade.protocol.TradeProtocol;
|
||||
import haveno.core.xmr.wallet.TradeWalletService;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
|
@ -172,11 +173,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
|
||||
@Override
|
||||
public void cleanupDisputes() {
|
||||
|
||||
// remove disputes opened by arbitrator, which is not allowed
|
||||
Set<Dispute> toRemoves = new HashSet<>();
|
||||
List<Dispute> disputes = getDisputeList().getList();
|
||||
synchronized (disputes) {
|
||||
|
||||
// collect disputes to remove
|
||||
Set<Dispute> toRemoves = new HashSet<>();
|
||||
for (Dispute dispute : disputes) {
|
||||
|
||||
// get dispute's trade
|
||||
|
@ -186,15 +187,47 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
return;
|
||||
}
|
||||
|
||||
// collect dispute if owned by arbitrator
|
||||
// remove dispute if owned by arbitrator
|
||||
if (dispute.getTraderPubKeyRing().equals(trade.getArbitrator().getPubKeyRing())) {
|
||||
log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", trade.getId(), dispute.getId());
|
||||
toRemoves.add(dispute);
|
||||
}
|
||||
|
||||
// remove dispute if preparing
|
||||
if (trade.getDisputeState() == DisputeState.DISPUTE_PREPARING) {
|
||||
log.warn("Removing dispute for {} {} with disputeState={}, disputeId={}", trade.getClass().getSimpleName(), trade.getId(), trade.getDisputeState(), dispute.getId());
|
||||
toRemoves.add(dispute);
|
||||
}
|
||||
|
||||
// remove dispute if requested and not stored in mailbox
|
||||
if (trade.getDisputeState() == DisputeState.DISPUTE_REQUESTED) {
|
||||
boolean storedInMailbox = false;
|
||||
for (ChatMessage msg : dispute.getChatMessages()) {
|
||||
if (Boolean.TRUE.equals(msg.getStoredInMailboxProperty().get())) {
|
||||
storedInMailbox = true;
|
||||
log.info("Keeping dispute for {} {} with disputeState={}, disputeId={}. Stored in mailbox", trade.getClass().getSimpleName(), trade.getId(), trade.getDisputeState(), dispute.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!storedInMailbox) {
|
||||
log.warn("Removing dispute for {} {} with disputeState={}, disputeId={}. Not stored in mailbox", trade.getClass().getSimpleName(), trade.getId(), trade.getDisputeState(), dispute.getId());
|
||||
toRemoves.add(dispute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove disputes and reset state
|
||||
for (Dispute dispute : toRemoves) {
|
||||
getDisputeList().remove(dispute);
|
||||
|
||||
// get dispute's trade
|
||||
final Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
if (trade == null) {
|
||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||
continue;
|
||||
}
|
||||
trade.setDisputeState(DisputeState.NO_DISPUTE);
|
||||
}
|
||||
}
|
||||
for (Dispute toRemove : toRemoves) {
|
||||
log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", toRemove.getTradeId(), toRemove.getId());
|
||||
getDisputeList().remove(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -169,6 +169,25 @@ public final class ChatMessage extends SupportMessage {
|
|||
false);
|
||||
}
|
||||
|
||||
public ChatMessage copy() {
|
||||
return new ChatMessage(supportType,
|
||||
tradeId,
|
||||
traderId,
|
||||
senderIsTrader,
|
||||
message,
|
||||
new ArrayList<>(attachments),
|
||||
senderNodeAddress,
|
||||
date,
|
||||
arrivedProperty.get(),
|
||||
storedInMailboxProperty.get(),
|
||||
uid,
|
||||
messageVersion,
|
||||
acknowledgedProperty.get(),
|
||||
sendMessageErrorProperty.get(),
|
||||
ackErrorProperty.get(),
|
||||
wasDisplayed);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
|
|
|
@ -77,6 +77,11 @@ public class TraderChatManager extends SupportManager {
|
|||
tradeManager.requestPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persistNow(Runnable completeHandler) {
|
||||
tradeManager.persistNow(completeHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
||||
return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> {
|
||||
|
|
|
@ -298,6 +298,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
|||
|
||||
public enum DisputeState {
|
||||
NO_DISPUTE,
|
||||
DISPUTE_PREPARING,
|
||||
DISPUTE_REQUESTED,
|
||||
DISPUTE_OPENED,
|
||||
ARBITRATOR_SENT_DISPUTE_CLOSED_MSG,
|
||||
|
@ -324,19 +325,18 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
|||
return protobuf.Trade.DisputeState.valueOf(disputeState.name());
|
||||
}
|
||||
|
||||
public boolean isNotDisputed() {
|
||||
return this == Trade.DisputeState.NO_DISPUTE;
|
||||
}
|
||||
|
||||
public boolean isMediated() {
|
||||
return this == Trade.DisputeState.MEDIATION_REQUESTED ||
|
||||
this == Trade.DisputeState.MEDIATION_STARTED_BY_PEER ||
|
||||
this == Trade.DisputeState.MEDIATION_CLOSED;
|
||||
}
|
||||
|
||||
public boolean isArbitrated() {
|
||||
if (isMediated()) return false; // TODO: remove mediation?
|
||||
return this.ordinal() >= DisputeState.DISPUTE_REQUESTED.ordinal();
|
||||
public boolean isDisputed() {
|
||||
return this.ordinal() >= DisputeState.DISPUTE_PREPARING.ordinal();
|
||||
}
|
||||
|
||||
public boolean isPreparing() {
|
||||
return this == DisputeState.DISPUTE_PREPARING;
|
||||
}
|
||||
|
||||
public boolean isRequested() {
|
||||
|
@ -720,7 +720,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
|||
});
|
||||
|
||||
// complete disputed trade
|
||||
if (getDisputeState().isArbitrated() && !getDisputeState().isClosed()) {
|
||||
if (getDisputeState().isDisputed() && !getDisputeState().isClosed()) {
|
||||
processModel.getTradeManager().closeDisputedTrade(getId(), Trade.DisputeState.DISPUTE_CLOSED);
|
||||
if (!isArbitrator()) for (Dispute dispute : getDisputes()) dispute.setIsClosed(); // auto close trader tickets
|
||||
}
|
||||
|
@ -1215,6 +1215,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
|||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
if (isShutDownStarted && wallet == null) {
|
||||
log.warn("Aborting import of multisig hex for {} {} because shut down is started and wallet is closed", getClass().getSimpleName(), getShortId());
|
||||
break;
|
||||
}
|
||||
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||
handleWalletError(e, sourceConnection, i + 1);
|
||||
doPollWallet();
|
||||
|
|
|
@ -67,6 +67,7 @@ import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
|||
import haveno.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import haveno.core.support.dispute.messages.DisputeClosedMessage;
|
||||
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
|
||||
import haveno.core.support.messages.ChatMessage;
|
||||
import haveno.core.trade.Trade.DisputeState;
|
||||
import haveno.core.trade.failed.FailedTradesManager;
|
||||
import haveno.core.trade.handlers.TradeResultHandler;
|
||||
|
@ -196,6 +197,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
PaymentSentMessage.class,
|
||||
PaymentReceivedMessage.class,
|
||||
DisputeOpenedMessage.class,
|
||||
ChatMessage.class,
|
||||
DisputeClosedMessage.class);
|
||||
|
||||
@Override
|
||||
|
@ -459,13 +461,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
if (!trade.isArbitrator()) continue;
|
||||
if (trade.isIdling()) {
|
||||
ThreadUtils.submitToPool(() -> {
|
||||
|
||||
// add random delay to avoid syncing at exactly the same time
|
||||
if (trades.size() > 1 && trade.walletExists()) {
|
||||
int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS);
|
||||
HavenoUtils.waitFor(delay);
|
||||
}
|
||||
|
||||
trade.syncAndPollWallet();
|
||||
});
|
||||
}
|
||||
|
@ -536,12 +531,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
}
|
||||
}
|
||||
|
||||
// add random delay to avoid syncing at exactly the same time
|
||||
if (trades.size() > 1 && trade.walletExists()) {
|
||||
int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS);
|
||||
HavenoUtils.waitFor(delay);
|
||||
}
|
||||
|
||||
// initialize trade
|
||||
initTrade(trade);
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
|||
if (!trade.isPayoutConfirmed()) processPayoutTx(message);
|
||||
|
||||
// close open disputes
|
||||
if (trade.isPayoutPublished() && trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_REQUESTED.ordinal()) {
|
||||
if (trade.isPayoutPublished() && trade.getDisputeState().isDisputed()) {
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||
for (Dispute dispute : trade.getDisputes()) dispute.setIsClosed();
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
|||
}
|
||||
|
||||
// close open disputes
|
||||
if (trade.isPayoutPublished() && trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_REQUESTED.ordinal()) {
|
||||
if (trade.isPayoutPublished() && trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_PREPARING.ordinal()) {
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||
for (Dispute dispute : trade.getDisputes()) dispute.setIsClosed();
|
||||
}
|
||||
|
|
|
@ -1216,6 +1216,7 @@ support.role=Role
|
|||
support.agent=Support agent
|
||||
support.state=State
|
||||
support.chat=Chat
|
||||
support.preparing=Preparing
|
||||
support.requested=Requested
|
||||
support.closed=Closed
|
||||
support.open=Open
|
||||
|
|
|
@ -1180,7 +1180,9 @@ support.agent=Agent podpory
|
|||
support.state=Stav
|
||||
support.chat=Chat
|
||||
support.requested=Požádáno
|
||||
support.closed=Zavřeno
|
||||
support.preparing=Připravuje se
|
||||
support.requested=Požadováno
|
||||
support.closed=Uzavřeno
|
||||
support.open=Otevřeno
|
||||
support.moreButton=VÍCE...
|
||||
support.sendLogFiles=Odeslat soubory logů
|
||||
|
|
|
@ -971,6 +971,8 @@ support.role=Rolle
|
|||
support.agent=Support-Mitarbeiter
|
||||
support.state=Status
|
||||
support.chat=Chat
|
||||
support.preparing=In Vorbereitung
|
||||
support.requested=Angefragt
|
||||
support.closed=Geschlossen
|
||||
support.open=Offen
|
||||
support.process=Process
|
||||
|
|
|
@ -972,6 +972,8 @@ support.role=Rol
|
|||
support.agent=Agente de soporte
|
||||
support.state=Estado
|
||||
support.chat=Chat
|
||||
support.preparing=Preparando
|
||||
support.requested=Solicitado
|
||||
support.closed=Cerrado
|
||||
support.open=Abierto
|
||||
support.process=Proceso
|
||||
|
|
|
@ -969,6 +969,8 @@ support.role=نقش
|
|||
support.agent=Support agent
|
||||
support.state=حالت
|
||||
support.chat=Chat
|
||||
support.preparing=در حال آمادهسازی
|
||||
support.requested=درخواست شده
|
||||
support.closed=بسته
|
||||
support.open=باز
|
||||
support.process=Process
|
||||
|
|
|
@ -973,6 +973,8 @@ support.role=Rôle
|
|||
support.agent=Agent d'assistance
|
||||
support.state=État
|
||||
support.chat=Chat
|
||||
support.preparing=Préparation
|
||||
support.requested=Demandé
|
||||
support.closed=Fermé
|
||||
support.open=Ouvert
|
||||
support.process=Processus
|
||||
|
|
|
@ -970,6 +970,8 @@ support.role=Ruolo
|
|||
support.agent=Support agent
|
||||
support.state=Stato
|
||||
support.chat=Chat
|
||||
support.preparing=In preparazione
|
||||
support.requested=Richiesto
|
||||
support.closed=Chiuso
|
||||
support.open=Aperto
|
||||
support.process=Process
|
||||
|
|
|
@ -971,7 +971,9 @@ support.role=役割
|
|||
support.agent=サポート代理人
|
||||
support.state=状態
|
||||
support.chat=Chat
|
||||
support.closed=クローズ
|
||||
support.preparing=準備中
|
||||
support.requested=リクエスト済
|
||||
support.closed=閉鎖
|
||||
support.open=オープン
|
||||
support.process=Process
|
||||
support.buyerMaker=XMR 買い手/メイカー
|
||||
|
|
|
@ -972,6 +972,8 @@ support.role=Função
|
|||
support.agent=Support agent
|
||||
support.state=Estado
|
||||
support.chat=Chat
|
||||
support.preparing=Preparando
|
||||
support.requested=Solicitado
|
||||
support.closed=Fechado
|
||||
support.open=Aberto
|
||||
support.process=Process
|
||||
|
|
|
@ -969,6 +969,8 @@ support.role=Cargo
|
|||
support.agent=Support agent
|
||||
support.state=Estado
|
||||
support.chat=Chat
|
||||
support.preparing=Preparando
|
||||
support.requested=Solicitado
|
||||
support.closed=Fechado
|
||||
support.open=Aberto
|
||||
support.process=Process
|
||||
|
|
|
@ -969,6 +969,8 @@ support.role=Роль
|
|||
support.agent=Support agent
|
||||
support.state=Состояние
|
||||
support.chat=Chat
|
||||
support.preparing=В процессе
|
||||
support.requested=Запрошено
|
||||
support.closed=Закрыто
|
||||
support.open=Открыто
|
||||
support.process=Process
|
||||
|
|
|
@ -969,6 +969,8 @@ support.role=บทบาท
|
|||
support.agent=Support agent
|
||||
support.state=สถานะ
|
||||
support.chat=Chat
|
||||
support.preparing=กำลังเตรียม
|
||||
support.requested=ร้องขอ
|
||||
support.closed=ปิดแล้ว
|
||||
support.open=เปิด
|
||||
support.process=Process
|
||||
|
|
|
@ -1175,6 +1175,7 @@ support.role=Rol
|
|||
support.agent=Destek temsilcisi
|
||||
support.state=Durum
|
||||
support.chat=Sohbet
|
||||
support.preparing=Hazırlanıyor
|
||||
support.requested=Talep edildi
|
||||
support.closed=Kapalı
|
||||
support.open=Açık
|
||||
|
|
|
@ -971,6 +971,8 @@ support.role=Vai trò
|
|||
support.agent=Support agent
|
||||
support.state=Trạng thái
|
||||
support.chat=Chat
|
||||
support.preparing=Đang chuẩn bị
|
||||
support.requested=Yêu cầu
|
||||
support.closed=Đóng
|
||||
support.open=Mở
|
||||
support.process=Process
|
||||
|
|
|
@ -971,6 +971,8 @@ support.role=角色
|
|||
support.agent=Support agent
|
||||
support.state=状态
|
||||
support.chat=Chat
|
||||
support.preparing=准备中
|
||||
support.requested=请求
|
||||
support.closed=关闭
|
||||
support.open=打开
|
||||
support.process=Process
|
||||
|
|
|
@ -971,6 +971,8 @@ support.role=角色
|
|||
support.agent=Support agent
|
||||
support.state=狀態
|
||||
support.chat=Chat
|
||||
support.preparing=準備中
|
||||
support.requested=已請求
|
||||
support.closed=關閉
|
||||
support.open=打開
|
||||
support.process=Process
|
||||
|
|
|
@ -19,6 +19,8 @@ package haveno.desktop.main.overlays.notifications;
|
|||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.core.api.NotificationListener;
|
||||
import haveno.core.locale.Res;
|
||||
|
@ -135,7 +137,7 @@ public class NotificationCenter {
|
|||
log.debug("We have already an entry in disputeStateSubscriptionsMap.");
|
||||
} else {
|
||||
Subscription disputeStateSubscription = EasyBind.subscribe(trade.disputeStateProperty(),
|
||||
disputeState -> onDisputeStateChanged(trade, disputeState));
|
||||
disputeState -> ThreadUtils.submitToPool(() -> onDisputeStateChanged(trade, disputeState)));
|
||||
disputeStateSubscriptionsMap.put(tradeId, disputeStateSubscription);
|
||||
}
|
||||
|
||||
|
@ -153,7 +155,7 @@ public class NotificationCenter {
|
|||
tradeManager.getObservableList().forEach(trade -> {
|
||||
String tradeId = trade.getId();
|
||||
Subscription disputeStateSubscription = EasyBind.subscribe(trade.disputeStateProperty(),
|
||||
disputeState -> onDisputeStateChanged(trade, disputeState));
|
||||
disputeState -> ThreadUtils.submitToPool(() -> onDisputeStateChanged(trade, disputeState)));
|
||||
disputeStateSubscriptionsMap.put(tradeId, disputeStateSubscription);
|
||||
|
||||
Subscription tradePhaseSubscription = EasyBind.subscribe(trade.statePhaseProperty(),
|
||||
|
@ -267,7 +269,7 @@ public class NotificationCenter {
|
|||
if (message != null) {
|
||||
goToSupport(trade, message, trade.isArbitrator() ? ArbitratorView.class : ArbitrationClientView.class);
|
||||
}
|
||||
}else if (refundManager.findDispute(trade.getId()).isPresent()) {
|
||||
} else if (refundManager.findDispute(trade.getId()).isPresent()) {
|
||||
String disputeOrTicket = refundManager.findDispute(trade.getId()).get().isSupportTicket() ?
|
||||
Res.get("shared.supportTicket") :
|
||||
Res.get("shared.dispute");
|
||||
|
|
|
@ -437,25 +437,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
return;
|
||||
}
|
||||
|
||||
// We do not support opening a dispute if the deposit tx is null. Traders have to use the support channel at keybase
|
||||
// in such cases. The mediators or arbitrators could not help anyway with a payout in such cases.
|
||||
String depositTxId = null;
|
||||
if (isMaker) {
|
||||
if (trade.getMaker().getDepositTxHash() == null) {
|
||||
log.error("Deposit tx must not be null");
|
||||
new Popup().instruction(Res.get("portfolio.pending.error.depositTxNull")).show();
|
||||
return;
|
||||
}
|
||||
depositTxId = trade.getMaker().getDepositTxHash();
|
||||
} else {
|
||||
if (trade.getTaker().getDepositTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) {
|
||||
log.error("Deposit tx must not be null");
|
||||
new Popup().instruction(Res.get("portfolio.pending.error.depositTxNull")).show();
|
||||
return;
|
||||
}
|
||||
depositTxId = trade.getTaker().getDepositTxHash();
|
||||
}
|
||||
|
||||
Offer offer = trade.getOffer();
|
||||
if (offer == null) {
|
||||
log.warn("offer is null at doOpenDispute");
|
||||
|
@ -576,8 +557,9 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
}
|
||||
|
||||
private void doSendDisputeOpenedMessage(Dispute dispute, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
|
||||
navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class);
|
||||
disputeManager.sendDisputeOpenedMessage(dispute,
|
||||
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class),
|
||||
null,
|
||||
(errorMessage, throwable) -> new Popup().warning(errorMessage).show());
|
||||
}
|
||||
|
||||
|
|
|
@ -274,12 +274,8 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
}
|
||||
|
||||
private void openSupportTicket() {
|
||||
if (trade.isDepositTxMissing() || trade.getPhase().ordinal() >= Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) {
|
||||
applyOnDisputeOpened();
|
||||
model.dataModel.onOpenDispute();
|
||||
} else {
|
||||
new Popup().warning(Res.get("portfolio.pending.error.depositTxNotConfirmed")).show();
|
||||
}
|
||||
applyOnDisputeOpened();
|
||||
model.dataModel.onOpenDispute();
|
||||
}
|
||||
|
||||
private void openChat() {
|
||||
|
@ -494,6 +490,7 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
case NO_DISPUTE:
|
||||
break;
|
||||
|
||||
case DISPUTE_PREPARING:
|
||||
case DISPUTE_REQUESTED:
|
||||
case DISPUTE_OPENED:
|
||||
if (tradeStepInfo != null) {
|
||||
|
@ -770,7 +767,7 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
}
|
||||
|
||||
private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) {
|
||||
if (!trade.getDisputeState().isOpen()) {
|
||||
if (!trade.getDisputeState().isDisputed()) {
|
||||
switch (tradePeriodState) {
|
||||
case FIRST_HALF:
|
||||
// just for dev testing. not possible to go back in time ;-)
|
||||
|
|
|
@ -79,7 +79,7 @@ public class BuyerStep4View extends TradeStepView {
|
|||
TitledGroupBg completedTradeLabel = new TitledGroupBg();
|
||||
if (trade.getDisputeState().isMediated()) {
|
||||
completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.mediated"));
|
||||
} else if (trade.getDisputeState().isArbitrated() && trade.getDisputeResult() != null) {
|
||||
} else if (trade.getDisputeState().isDisputed() && trade.getDisputeResult() != null) {
|
||||
completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.arbitrated"));
|
||||
} else {
|
||||
completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle"));
|
||||
|
|
|
@ -214,14 +214,15 @@ public class ChatView extends AnchorPane {
|
|||
sendButton = new AutoTooltipButton(Res.get("support.send"));
|
||||
sendButton.setDefaultButton(true);
|
||||
sendButton.setOnAction(e -> onTrySendMessage());
|
||||
sendButton.setStyle("-fx-pref-width: 125; -fx-min-width: 110; -fx-padding: 3 3 3 3;");
|
||||
inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(), t -> sendButton.setDisable(t.isEmpty()));
|
||||
|
||||
Button uploadButton = new AutoTooltipButton(Res.get("support.addAttachments"));
|
||||
uploadButton.setOnAction(e -> onRequestUpload());
|
||||
Button clipboardButton = new AutoTooltipButton(Res.get("shared.copyToClipboard"));
|
||||
clipboardButton.setOnAction(e -> copyChatMessagesToClipboard(clipboardButton));
|
||||
uploadButton.setStyle("-fx-pref-width: 125; -fx-padding: 3 3 3 3;");
|
||||
clipboardButton.setStyle("-fx-pref-width: 125; -fx-padding: 3 3 3 3;");
|
||||
uploadButton.setStyle("-fx-pref-width: 125; -fx-min-width: 110; -fx-padding: 3 3 3 3;");
|
||||
clipboardButton.setStyle("-fx-pref-width: 125; -fx-min-width: 110; -fx-padding: 3 3 3 3;");
|
||||
|
||||
sendMsgInfoLabel = new AutoTooltipLabel();
|
||||
sendMsgInfoLabel.setVisible(false);
|
||||
|
|
|
@ -125,7 +125,7 @@ public class DisputeChatPopup {
|
|||
menuItem2.setOnAction(e -> chatCallback.onSendLogsFromChatWindow(selectedDispute));
|
||||
menuButton.getItems().addAll(menuItem1, menuItem2);
|
||||
menuButton.getStyleClass().add("jfx-button");
|
||||
menuButton.setStyle("-fx-padding: 0 10 0 10;");
|
||||
menuButton.setStyle("-fx-min-width: 95; -fx-padding: 0 10 0 10;");
|
||||
chatView.display(concreteDisputeSession, menuButton, pane.widthProperty());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -332,6 +332,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
|||
TableRow<Dispute> row = new TableRow<>();
|
||||
row.setOnMouseClicked(event -> {
|
||||
if (event.getClickCount() == 2 && (!row.isEmpty())) {
|
||||
if (!canViewChatMessages(row.getItem())) return;
|
||||
openChat(row.getItem());
|
||||
}
|
||||
});
|
||||
|
@ -1045,10 +1046,17 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
|||
@Override
|
||||
public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
|
||||
return new TableCell<>() {
|
||||
|
||||
Subscription subscription;
|
||||
|
||||
@Override
|
||||
public void updateItem(final Dispute item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null && !empty) {
|
||||
if (subscription != null) {
|
||||
subscription.unsubscribe();
|
||||
subscription = null;
|
||||
}
|
||||
String id = item.getId();
|
||||
Button button;
|
||||
if (!chatButtonByDispute.containsKey(id)) {
|
||||
|
@ -1075,10 +1083,22 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
|||
listenerByDispute.put(id, listener);
|
||||
item.getChatMessages().addListener(listener);
|
||||
}
|
||||
|
||||
// subscribe to trade's dispute state
|
||||
Trade trade = tradeManager.getTrade(item.getTradeId());
|
||||
if (trade == null) log.warn("Dispute's trade is null for trade {}", item.getTradeId());
|
||||
else subscription = EasyBind.subscribe(trade.disputeStateProperty(), disputeState -> {
|
||||
chatBadge.setDisable(!canViewChatMessages(item));
|
||||
updateChatMessageCount(item, chatBadge);
|
||||
});
|
||||
updateChatMessageCount(item, chatBadge);
|
||||
setGraphic(chatBadge);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (subscription != null) {
|
||||
subscription.unsubscribe();
|
||||
subscription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1356,7 +1376,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
|||
public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
|
||||
return new TableCell<>() {
|
||||
|
||||
|
||||
ReadOnlyBooleanProperty closedProperty;
|
||||
ChangeListener<Boolean> listener;
|
||||
Subscription subscription;
|
||||
|
@ -1419,6 +1438,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
|||
switch (trade.getDisputeState()) {
|
||||
case NO_DISPUTE:
|
||||
return Res.get("shared.pending");
|
||||
case DISPUTE_PREPARING:
|
||||
return Res.get("support.preparing");
|
||||
case DISPUTE_REQUESTED:
|
||||
return Res.get("support.requested");
|
||||
default:
|
||||
|
@ -1447,7 +1468,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
|||
return;
|
||||
}
|
||||
|
||||
if (dispute.unreadMessageCount(senderFlag()) > 0) {
|
||||
if (canViewChatMessages(dispute) && dispute.unreadMessageCount(senderFlag()) > 0) {
|
||||
chatBadge.setText(String.valueOf(dispute.unreadMessageCount(senderFlag())));
|
||||
chatBadge.setEnabled(true);
|
||||
} else {
|
||||
|
@ -1498,4 +1519,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
|||
ArbitrationManager arbitrationManager = (ArbitrationManager) disputeManager;
|
||||
new SendLogFilesWindow(dispute.getTradeId(), dispute.getTraderId(), arbitrationManager).show();
|
||||
}
|
||||
|
||||
private boolean canViewChatMessages(Dispute dispute) {
|
||||
return disputeManager.canSendChatMessages(dispute) || dispute.isClosed();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1495,6 +1495,7 @@ message Trade {
|
|||
enum DisputeState {
|
||||
PB_ERROR_DISPUTE_STATE = 0;
|
||||
NO_DISPUTE = 1;
|
||||
DISPUTE_PREPARING = 15;
|
||||
DISPUTE_REQUESTED = 2;
|
||||
DISPUTE_OPENED = 3;
|
||||
ARBITRATOR_SENT_DISPUTE_CLOSED_MSG = 4;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue